Skip to content

Commit 3e77496

Browse files
authored
Add JAR utilities; add JavaDoc to existing classes and methods
1 parent 7262525 commit 3e77496

File tree

5 files changed

+204
-0
lines changed

5 files changed

+204
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ integration/felix-cache/
1212
runner
1313
.DS_Store
1414
test-output
15+
derby.log

src/main/java/com/nordstrom/common/base/ExceptionUnwrapper.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
import java.util.HashSet;
77
import java.util.Set;
88

9+
/**
10+
* This utility class provides methods for extracting the contents of "wrapped" exceptions.
11+
*/
912
public class ExceptionUnwrapper {
1013

1114
private static final Set<Class<? extends Exception>> unwrappable =

src/main/java/com/nordstrom/common/file/VolumeInfo.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212

1313
import com.nordstrom.common.file.OSInfo.OSType;
1414

15+
/**
16+
* This utility class provides methods that parse the output of the 'mount' utility into a mapped collection of
17+
* volume property records.
18+
*/
1519
public class VolumeInfo {
1620

1721
static final boolean IS_WINDOWS = (OSInfo.getDefault().getType() == OSType.WINDOWS);
@@ -20,6 +24,12 @@ private VolumeInfo() {
2024
throw new AssertionError("VolumeInfo is a static utility class that cannot be instantiated");
2125
}
2226

27+
/**
28+
* Invoke the 'mount' utility and return its output as a mapped collection of volume property records.
29+
*
30+
* @return map of {@link VolumeProps} objects
31+
* @throws IOException if an I/O error occurs
32+
*/
2333
public static Map<String, VolumeProps> getVolumeProps() throws IOException {
2434
Process mountProcess;
2535
if (IS_WINDOWS) {
@@ -31,6 +41,15 @@ public static Map<String, VolumeProps> getVolumeProps() throws IOException {
3141
return getVolumeProps(mountProcess.getInputStream());
3242
}
3343

44+
/**
45+
* Parse the content of the provided input stream into a mapped collection of volume property records.
46+
* <p>
47+
* <b>NOTE</b>: This method assumes that the provided content was produced by the 'mount' utility.
48+
*
49+
* @param is {@link InputStream} emitted by the 'mount' utility
50+
* @return map of {@link VolumeProps} objects
51+
* @throws IOException if an I/O error occurs
52+
*/
3453
public static Map<String, VolumeProps> getVolumeProps(InputStream is) throws IOException {
3554
Map<String, VolumeProps> propsList = new HashMap<>();
3655
Pattern template = Pattern.compile("(.+) on (.+) type (.+) \\((.+)\\)");
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.nordstrom.common.jar;
2+
3+
import static org.testng.Assert.assertEquals;
4+
import static org.testng.Assert.assertTrue;
5+
6+
import java.io.File;
7+
import org.testng.annotations.Test;
8+
9+
public class JarUtilsTest {
10+
11+
private static String[] CONTEXTS = { "org.testng.annotations.Test", "com.beust.jcommander.JCommander",
12+
"org.apache.derby.jdbc.EmbeddedDriver", "com.google.common.base.Charsets" };
13+
14+
@Test
15+
public void testClasspath() {
16+
String result = JarUtils.getClasspath(CONTEXTS);
17+
String[] paths = result.split(";");
18+
assertEquals(paths.length, CONTEXTS.length, "path entry count mismatch");
19+
for (String thisPath : paths) {
20+
File file = new File(thisPath);
21+
assertTrue(file.exists(), "nonexistent path entry: " + thisPath);
22+
}
23+
}
24+
25+
}

0 commit comments

Comments
 (0)