|
| 1 | +package com.nordstrom.common.jar; |
| 2 | + |
| 3 | +import java.io.File; |
| 4 | +import java.io.FileInputStream; |
| 5 | +import java.io.IOException; |
| 6 | +import java.io.InputStream; |
| 7 | +import java.io.UnsupportedEncodingException; |
| 8 | +import java.net.URLDecoder; |
| 9 | +import java.nio.charset.Charset; |
| 10 | +import java.util.HashSet; |
| 11 | +import java.util.Set; |
| 12 | +import java.util.jar.JarInputStream; |
| 13 | +import java.util.jar.Manifest; |
| 14 | + |
| 15 | +import com.google.common.base.Joiner; |
| 16 | +import com.nordstrom.common.base.UncheckedThrow; |
| 17 | + |
| 18 | +/** |
| 19 | + * This utility class provides methods related to Java JAR files: |
| 20 | + * |
| 21 | + * <ul> |
| 22 | + * <li>{@link #getClasspath} assemble a classpath string from the specified array of dependencies.</li> |
| 23 | + * <li>{@link #findJarPathFor} find the path to the JAR file from which the named class was loaded.</li> |
| 24 | + * <li>{@link #getJarPremainClass} gets the 'Premain-Class' attribute from the indicated JAR file.</li> |
| 25 | + * </ul> |
| 26 | + */ |
| 27 | +public class JarUtils { |
| 28 | + |
| 29 | + /** |
| 30 | + * Assemble a classpath string from the specified array of dependencies. |
| 31 | + * <p> |
| 32 | + * <b>NOTE</b>: If any of the specified dependency contexts names the {@code premain} class of a Java agent, the |
| 33 | + * string returned by this method will contain two records delimited by a {@code newline} character: |
| 34 | + * |
| 35 | + * <ul> |
| 36 | + * <li>0 - assembled classpath string</li> |
| 37 | + * <li>1 - tab-delimited list of Java agent paths</li> |
| 38 | + * </ul> |
| 39 | + * |
| 40 | + * @param dependencyContexts array of dependency contexts |
| 41 | + * @return assembled classpath string (see <b>NOTE</b>) |
| 42 | + */ |
| 43 | + public static String getClasspath(final String[] dependencyContexts) { |
| 44 | + Set<String> agentList = new HashSet<>(); |
| 45 | + Set<String> pathList = new HashSet<>(); |
| 46 | + for (String contextClassName : dependencyContexts) { |
| 47 | + // get JAR path for this dependency context |
| 48 | + String jarPath = findJarPathFor(contextClassName); |
| 49 | + // if this context names the premain class of a Java agent |
| 50 | + if (contextClassName.equals(getJarPremainClass(jarPath))) { |
| 51 | + // collect agent path |
| 52 | + agentList.add(jarPath); |
| 53 | + // otherwise |
| 54 | + } else { |
| 55 | + // collect class path |
| 56 | + pathList.add(jarPath); |
| 57 | + } |
| 58 | + } |
| 59 | + // assemble classpath string |
| 60 | + String classPath = Joiner.on(File.pathSeparator).join(pathList); |
| 61 | + // if no agents were found |
| 62 | + if (agentList.isEmpty()) { |
| 63 | + // classpath only |
| 64 | + return classPath; |
| 65 | + } else { |
| 66 | + // classpath plus tab-delimited list of agent paths |
| 67 | + return classPath + "\n" + Joiner.on("\t").join(agentList); |
| 68 | + } |
| 69 | + } |
| 70 | + |
| 71 | + /** |
| 72 | + * If the provided class has been loaded from a JAR file that is on the |
| 73 | + * local file system, will find the absolute path to that JAR file. |
| 74 | + * |
| 75 | + * @param contextClassName |
| 76 | + * The JAR file that contained the class file that represents |
| 77 | + * this class will be found. |
| 78 | + * @return absolute path to the JAR file from which the specified class was |
| 79 | + * loaded |
| 80 | + * @throws IllegalStateException |
| 81 | + * If the specified class was loaded from a directory or in some |
| 82 | + * other way (such as via HTTP, from a database, or some other |
| 83 | + * custom class-loading device). |
| 84 | + */ |
| 85 | + public static String findJarPathFor(final String contextClassName) { |
| 86 | + Class<?> contextClass; |
| 87 | + |
| 88 | + try { |
| 89 | + contextClass = Class.forName(contextClassName); |
| 90 | + } catch (ClassNotFoundException e) { |
| 91 | + throw UncheckedThrow.throwUnchecked(e); |
| 92 | + } |
| 93 | + |
| 94 | + String shortName = contextClassName; |
| 95 | + int idx = shortName.lastIndexOf('.'); |
| 96 | + String protocol; |
| 97 | + |
| 98 | + if (idx > -1) { |
| 99 | + shortName = shortName.substring(idx + 1); |
| 100 | + } |
| 101 | + |
| 102 | + String uri = contextClass.getResource(shortName + ".class").toString(); |
| 103 | + |
| 104 | + if (uri.startsWith("file:")) { |
| 105 | + protocol = "file:"; |
| 106 | + String relPath = '/' + contextClassName.replace('.', '/') + ".class"; |
| 107 | + if (uri.endsWith(relPath)) { |
| 108 | + idx = uri.length() - relPath.length(); |
| 109 | + } else { |
| 110 | + throw new IllegalStateException( |
| 111 | + "This class has been loaded from a class file, but I can't make sense of the path!"); |
| 112 | + } |
| 113 | + } else if (uri.startsWith("jar:file:")) { |
| 114 | + protocol = "jar:file:"; |
| 115 | + idx = uri.indexOf('!'); |
| 116 | + if (idx == -1) { |
| 117 | + throw new IllegalStateException( |
| 118 | + "You appear to have loaded this class from a local jar file, but I can't make sense of the URL!"); |
| 119 | + } |
| 120 | + } else { |
| 121 | + idx = uri.indexOf(':'); |
| 122 | + protocol = (idx > -1) ? uri.substring(0, idx) : "(unknown)"; |
| 123 | + throw new IllegalStateException("This class has been loaded remotely via the " + protocol |
| 124 | + + " protocol. Only loading from a jar on the local file system is supported."); |
| 125 | + } |
| 126 | + |
| 127 | + try { |
| 128 | + String fileName = URLDecoder.decode(uri.substring(protocol.length(), idx), |
| 129 | + Charset.defaultCharset().name()); |
| 130 | + return new File(fileName).getAbsolutePath(); |
| 131 | + } catch (UnsupportedEncodingException e) { |
| 132 | + throw (InternalError) new InternalError( |
| 133 | + "Default charset doesn't exist. Your VM is borked.").initCause(e); |
| 134 | + } |
| 135 | + } |
| 136 | + |
| 137 | + /** |
| 138 | + * Extract the 'Premain-Class' attribute from the manifest of the indicated JAR file. |
| 139 | + * |
| 140 | + * @param jarPath absolute path to the JAR file |
| 141 | + * @return value of 'Premain-Class' attribute; {@code null} if unspecified |
| 142 | + */ |
| 143 | + public static String getJarPremainClass(String jarPath) { |
| 144 | + try(InputStream inputStream = new FileInputStream(jarPath); |
| 145 | + JarInputStream jarStream = new JarInputStream(inputStream)) { |
| 146 | + Manifest manifest = jarStream.getManifest(); |
| 147 | + if (manifest != null) { |
| 148 | + return manifest.getMainAttributes().getValue("Premain-Class"); |
| 149 | + } |
| 150 | + } catch (IOException e) { |
| 151 | + // nothing to do here |
| 152 | + } |
| 153 | + return null; |
| 154 | + } |
| 155 | + |
| 156 | +} |
0 commit comments