Skip to content

Commit

Permalink
Add overloaded readAllMainAttributesFromJarManifest methods to KiwiJa…
Browse files Browse the repository at this point in the history
…rs (#1204)

* Add 3 overloaded methods named readAllMainAttributesFromJarManifest
which provide an easier way to get all main attributes from a JAR
manifest. Since these are new methods, I named them to make it clear
what they are getting from the manifest, at the expense of inconsistency
with the existing method names.
* Changed a log message to make it clearer when a specific main
attribute could not be found in a JAR manifest.
* A few minor Javadoc changes to add a space in the generic type
declaration.
  • Loading branch information
sleberknight authored Oct 12, 2024
1 parent c1eebfa commit 379d95d
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 22 deletions.
75 changes: 64 additions & 11 deletions src/main/java/org/kiwiproject/jar/KiwiJars.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -150,7 +149,7 @@ public static Optional<String> readSingleValueFromJarManifest(ClassLoader classL
var value = manifest.getMainAttributes().getValue(mainAttributeName);
return Optional.ofNullable(value);
} catch (Exception e) {
LOG.warn("Unable to locate {} from JAR", mainAttributeName, e);
LOG.warn("Unable to get main attribute {} from JAR manifest", mainAttributeName, e);
return Optional.empty();
}
}
Expand All @@ -159,7 +158,7 @@ public static Optional<String> readSingleValueFromJarManifest(ClassLoader classL
* Get the values of the given main attribute names from the manifest (if found) from the current class loader.
*
* @param mainAttributeNames an array of main attribute names to resolve from the manifest
* @return a {@code Map<String,String>} of resolved main attributes
* @return a {@code Map<String, String>} of resolved main attributes
*/
public static Map<String, String> readValuesFromJarManifest(String... mainAttributeNames) {
return readValuesFromJarManifest(KiwiJars.class.getClassLoader(), null, mainAttributeNames);
Expand All @@ -170,7 +169,7 @@ public static Map<String, String> readValuesFromJarManifest(String... mainAttrib
*
* @param classLoader the classloader to search for manifest files in
* @param mainAttributeNames an array of names to resolve from the manifest
* @return a {@code Map<String,String>} of resolved main attributes
* @return a {@code Map<String, String>} of resolved main attributes
*/
public static Map<String, String> readValuesFromJarManifest(ClassLoader classLoader,
String... mainAttributeNames) {
Expand All @@ -184,32 +183,79 @@ public static Map<String, String> readValuesFromJarManifest(ClassLoader classLoa
* @param classLoader the classloader to search for manifest files in
* @param manifestFilter a predicate filter used to limit which jar files to search for a manifest file
* @param mainAttributeNames an array of names to resolve from the manifest
* @return a {@code Map<String,String>} of resolved main attributes
* @return a {@code Map<String, String>} of resolved main attributes
* @implNote If this code is called from a "fat-jar" with a single manifest file, then the filter predicate is unnecessary.
* The predicate filter is really only necessary if there are multiple jars loaded in the classpath all containing manifest files.
*/
public static Map<String, String> readValuesFromJarManifest(ClassLoader classLoader,
@Nullable Predicate<URL> manifestFilter,
String... mainAttributeNames) {

var uniqueNames = Set.of(mainAttributeNames);
return readMainAttributesFromJarManifest(classLoader,
entry -> uniqueNames.contains(entry.getKey()),
manifestFilter);
}

/**
* Get the values of all main attributes from the manifest (if found) from the current class loader.
*
* @return a {@code Map<String, String>} of all main attributes
*/
public static Map<String, String> readAllMainAttributesFromJarManifest() {
return readAllMainAttributesFromJarManifest(KiwiJars.class.getClassLoader(), null);
}

/**
* Get the values of all main attributes from the manifest (if found) from the given class loader.
*
* @param classLoader the classloader to search for manifest files in
* @return a {@code Map<String, String>} of all main attributes
*/
public static Map<String, String> readAllMainAttributesFromJarManifest(ClassLoader classLoader) {
return readAllMainAttributesFromJarManifest(classLoader, null);
}

/**
* Get the values of all main attributes from the manifest (if found) from the given class loader
* and filtering manifests using the given Predicate.
*
* @param classLoader the classloader to search for manifest files in
* @param manifestFilter a predicate filter used to limit which jar files to search for a manifest file
* @return a {@code Map<String, String>} of all main attributes
*/
public static Map<String, String> readAllMainAttributesFromJarManifest(ClassLoader classLoader,
@Nullable Predicate<URL> manifestFilter) {
return readMainAttributesFromJarManifest(classLoader, entry -> true, manifestFilter);
}

private static Map<String, String> readMainAttributesFromJarManifest(ClassLoader classLoader,
Predicate<Map.Entry<String, String>> entryPredicate,
@Nullable Predicate<URL> manifestFilter) {
try {
var manifest = findManifestOrNull(classLoader, manifestFilter);
if (isNull(manifest)) {
return Map.of();
}

var uniqueNames = Set.of(mainAttributeNames);
return manifest.getMainAttributes()
.entrySet()
.stream()
.filter(e -> uniqueNames.contains(String.valueOf(e.getKey())))
.collect(toUnmodifiableMap(e -> String.valueOf(e.getKey()), e -> String.valueOf(e.getValue())));

.map(KiwiJars::toEntryOfStringToString)
.filter(entryPredicate)
.collect(toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue));

} catch (Exception e) {
LOG.warn("Unable to locate {} from JAR", Arrays.toString(mainAttributeNames), e);
LOG.warn("Unable to get main attributes from JAR manifest", e);
return Map.of();
}
}

private static Map.Entry<String, String> toEntryOfStringToString(Map.Entry<Object, Object> e) {
return Map.entry(String.valueOf(e.getKey()), String.valueOf(e.getValue()));
}

@Nullable
private static Manifest findManifestOrNull(ClassLoader classLoader,
@Nullable Predicate<URL> manifestFilter) throws IOException {

Expand Down Expand Up @@ -240,14 +286,21 @@ private static List<URL> findManifestUrls(ClassLoader classLoader,
}

@VisibleForTesting
@Nullable
static Manifest readFirstManifestOrNull(List<URL> urls) {
LOG.trace("Using manifest URL(s): {}", urls);

return urls.stream()
var manifest = urls.stream()
.map(KiwiJars::readManifest)
.flatMap(Optional::stream)
.findFirst()
.orElse(null);

if (isNull(manifest)) {
LOG.warn("Unable to get a manifest using URLs: {}", urls);
}

return manifest;
}

private static Optional<Manifest> readManifest(URL url) {
Expand Down
79 changes: 68 additions & 11 deletions src/test/java/org/kiwiproject/jar/KiwiJarsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import org.kiwiproject.junit.jupiter.ClearBoxTest;

import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
Expand Down Expand Up @@ -116,11 +116,8 @@ void shouldReturnItemsWithLeadingFileSeparator_WhenListHasEmptyFirstElement() {
class ReadSingleValueFromJarManifest {

@Test
void shouldReadAnActualValueFromTheManifest_WithGivenClassLoaderAndPredicate() throws IOException {
var classLoader = new URLClassLoader(
new URL[] {Fixtures.fixturePath("KiwiJars/KiwiTestSample.jar").toUri().toURL()},
this.getClass().getClassLoader()
);
void shouldReadAnActualValueFromTheManifest_WithGivenClassLoaderAndPredicate() {
var classLoader = urlClassLoaderForKiwiTestSampleJar();

var value = KiwiJars.readSingleValueFromJarManifest(classLoader, "Sample-Attribute", url -> url.getPath().contains("KiwiTestSample"));

Expand Down Expand Up @@ -161,11 +158,8 @@ void shouldReturnOptionalEmptyIfValueCouldNotBeFoundInManifest_UsingDefaultClass
class ReadValuesFromJarManifest {

@Test
void shouldReadActualValuesFromTheManifest_WithGivenClassLoaderAndPredicate() throws IOException {
var classLoader = new URLClassLoader(
new URL[] {Fixtures.fixturePath("KiwiJars/KiwiTestSample.jar").toUri().toURL()},
this.getClass().getClassLoader()
);
void shouldReadActualValuesFromTheManifest_WithGivenClassLoaderAndPredicate() {
var classLoader = urlClassLoaderForKiwiTestSampleJar();

var values = KiwiJars.readValuesFromJarManifest(classLoader,
url -> url.getPath().contains("KiwiTestSample"), "Sample-Attribute", "Main-Class");
Expand Down Expand Up @@ -216,6 +210,58 @@ void shouldReturnEmptyMap_WhenManifestCannotBeFound() {
}
}

@Nested
class ReadAllMainAttributesFromJarManifest {

@Test
void shouldReturnAllValuesFromTheManifest_WithDefaultClassLoader() {
var values = KiwiJars.readAllMainAttributesFromJarManifest();

assertThat(values).isNotEmpty();
}

@Test
void shouldReturnAllValuesFromTheManifest_WithGivenClassLoader() {
var classLoader = urlClassLoaderForKiwiTestSampleJar();

var values = KiwiJars.readAllMainAttributesFromJarManifest(classLoader);

assertThat(values).isNotEmpty();
}

@Test
void shouldReturnAllValuesFromTheManifest_WithGivenClassLoaderAndPredicate() {
var classLoader = urlClassLoaderForKiwiTestSampleJar();

var values = KiwiJars.readAllMainAttributesFromJarManifest(classLoader,
url -> url.getPath().contains("KiwiTestSample"));

assertThat(values).contains(
entry("Manifest-Version", "1.0"),
entry("Main-Class", "KiwiTestClass"),
entry("Sample-Attribute", "the-value"),
entry("Created-By", "11.0.2 (Oracle Corporation)")
);
}

@SuppressWarnings("ConstantValue")
@Test
void shouldReturnEmptyMap_ForInvalidClassLoader() {
ClassLoader classLoader = null;
var values = KiwiJars.readAllMainAttributesFromJarManifest(classLoader);

assertThat(values).isEmpty();
}

@Test
void shouldReturnEmptyMap_WhenManifestCannotBeFound() {
var values = KiwiJars.readAllMainAttributesFromJarManifest(this.getClass().getClassLoader(),
url -> false); // ensures manifest won't be found

assertThat(values).isEmpty();
}
}

@Nested
class ReadFirstManifestOrNull {

Expand All @@ -233,4 +279,15 @@ void shouldReturnNull_WhenUrlIsInvalid() throws MalformedURLException {
assertThat(manifest).isNull();
}
}

private static URLClassLoader urlClassLoaderForKiwiTestSampleJar() {
try {
return new URLClassLoader(
new URL[] { Fixtures.fixturePath("KiwiJars/KiwiTestSample.jar").toUri().toURL() },
KiwiJars.class.getClassLoader()
);
} catch (MalformedURLException e) {
throw new UncheckedIOException(e);
}
}
}

0 comments on commit 379d95d

Please sign in to comment.