Skip to content

Commit

Permalink
Refactor KiwiJars#readValuesFromJarManifest (#1200)
Browse files Browse the repository at this point in the history
* Refactor readValuesFromJarManifest so it only reads the Manifest once.
* Add Nullable annotation to the Predicate in several methods.
* Change singular variable name 'value' in KiwiJarsTest to plural
'values' in tests of readValuesFromJarManifest, so that it is clearer
that the result contains multiple values.

Closes #1199
sleberknight authored Sep 19, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 1eafa67 commit f2021aa
Showing 2 changed files with 125 additions and 54 deletions.
124 changes: 78 additions & 46 deletions src/main/java/org/kiwiproject/jar/KiwiJars.java
Original file line number Diff line number Diff line change
@@ -5,23 +5,26 @@
import static com.google.common.collect.Lists.newArrayList;
import static java.util.Collections.emptyList;
import static java.util.Objects.isNull;
import static java.util.stream.Collectors.toUnmodifiableMap;
import static org.kiwiproject.base.KiwiPreconditions.checkArgumentNotNull;
import static org.kiwiproject.collect.KiwiLists.isNotNullOrEmpty;
import static org.kiwiproject.collect.KiwiLists.isNullOrEmpty;

import com.google.common.annotations.VisibleForTesting;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.jar.Manifest;
import java.util.stream.StreamSupport;
@@ -130,54 +133,23 @@ public static Optional<String> readSingleValueFromJarManifest(ClassLoader classL
* The predicate filter is really only necessary if there are multiple jars loaded in the classpath all containing manifest files.
*/
@SuppressWarnings("java:S2259")
public static Optional<String> readSingleValueFromJarManifest(ClassLoader classLoader, String manifestEntryName, Predicate<URL> manifestFilter) {
public static Optional<String> readSingleValueFromJarManifest(ClassLoader classLoader,
String manifestEntryName,
@Nullable Predicate<URL> manifestFilter) {
try {

List<URL> urls;

if (isNull(manifestFilter)) {
var manifestUrl = Optional.ofNullable(classLoader.getResource("META-INF/MANIFEST.MF"));
urls = manifestUrl.map(List::of).orElse(null);
} else {
var urlIterator = classLoader.getResources("META-INF/MANIFEST.MF").asIterator();
Iterable<URL> urlIterable = () -> urlIterator;

urls = StreamSupport
.stream((urlIterable).spliterator(), false)
.filter(manifestFilter)
.toList();
}

LOG.trace("Using manifest URL(s): {}", urls);

if (isNullOrEmpty(urls)) {
var manifest = findManifestOrNull(classLoader, manifestFilter);
if (isNull(manifest)) {
return Optional.empty();
}

return urls.stream().map(url -> readEntry(url, manifestEntryName))
.flatMap(Optional::stream)
.findFirst();

var value = manifest.getMainAttributes().getValue(manifestEntryName);
return Optional.ofNullable(value);
} catch (Exception e) {
LOG.warn("Unable to locate {} from JAR", manifestEntryName, e);
return Optional.empty();
}
}

private static Optional<String> readEntry(URL url, String manifestEntryName) {
try (var in = url.openStream()) {
var manifest = new Manifest(in);
return readEntry(manifest, manifestEntryName);
} catch (Exception e) {
LOG.warn("Unable to read manifest", e);
return Optional.empty();
}
}

private static Optional<String> readEntry(Manifest manifest, String manifestEntryName) {
return Optional.ofNullable(manifest.getMainAttributes().getValue(manifestEntryName));
}

/**
* Resolves all the given entry names from the manifest (if found) from the current class loader.
*
@@ -209,14 +181,74 @@ public static Map<String, String> readValuesFromJarManifest(ClassLoader classLoa
* @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, Predicate<URL> manifestFilter, String... manifestEntryNames) {
var entries = new HashMap<String, String>();
public static Map<String, String> readValuesFromJarManifest(ClassLoader classLoader,
@Nullable Predicate<URL> manifestFilter,
String... manifestEntryNames) {
try {
var manifest = findManifestOrNull(classLoader, manifestFilter);
if (isNull(manifest)) {
return Map.of();
}

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

} catch (Exception e) {
LOG.warn("Unable to locate {} from JAR", Arrays.toString(manifestEntryNames), e);
return Map.of();
}
}

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

var urls = findManifestUrls(classLoader, manifestFilter);
if (isNullOrEmpty(urls)) {
LOG.warn("There are no manifest URLs!" +
" The ClassLoader may have returned no resources or the manifestFilter did not match any URLs.");
return null;
}

return readFirstManifestOrNull(urls);
}

private static List<URL> findManifestUrls(ClassLoader classLoader,
@Nullable Predicate<URL> manifestFilter) throws IOException {
if (isNull(manifestFilter)) {
var manifestUrl = Optional.ofNullable(classLoader.getResource("META-INF/MANIFEST.MF"));
return manifestUrl.map(List::of).orElseGet(List::of);
}

Arrays.stream(manifestEntryNames).forEach(manifestEntryName -> {
var entry = readSingleValueFromJarManifest(classLoader, manifestEntryName, manifestFilter);
entry.ifPresent(value -> entries.put(manifestEntryName, value));
});
var urlIterator = classLoader.getResources("META-INF/MANIFEST.MF").asIterator();
Iterable<URL> urlIterable = () -> urlIterator;

return entries;
return StreamSupport
.stream((urlIterable).spliterator(), false)
.filter(manifestFilter)
.toList();
}

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

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

private static Optional<Manifest> readManifest(URL url) {
try (var in = url.openStream()) {
return Optional.of(new Manifest(in));
} catch (Exception e) {
LOG.warn("Unable to read manifest from URL: {}", url, e);
return Optional.empty();
}
}
}
55 changes: 47 additions & 8 deletions src/test/java/org/kiwiproject/jar/KiwiJarsTest.java
Original file line number Diff line number Diff line change
@@ -13,9 +13,12 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.kiwiproject.internal.Fixtures;
import org.kiwiproject.junit.jupiter.ClearBoxTest;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.List;
@@ -164,34 +167,70 @@ void shouldReadActualValuesFromTheManifest_WithGivenClassLoaderAndPredicate() th
this.getClass().getClassLoader()
);

var value = KiwiJars.readValuesFromJarManifest(classLoader, url -> url.getPath().contains("KiwiTestSample"), "Sample-Attribute", "Main-Class");
var values = KiwiJars.readValuesFromJarManifest(classLoader,
url -> url.getPath().contains("KiwiTestSample"), "Sample-Attribute", "Main-Class");

assertThat(value).contains(
assertThat(values).contains(
entry("Sample-Attribute", "the-value"),
entry("Main-Class", "KiwiTestClass")
);
}

@Test
void shouldReturnEmptyMapIfValuesCouldNotBeFoundInManifest_UsingClassLoaderAndPredicate() {
var value = KiwiJars.readValuesFromJarManifest(this.getClass().getClassLoader(), url -> true, "foo");
var values = KiwiJars.readValuesFromJarManifest(this.getClass().getClassLoader(), url -> true, "foo");

assertThat(value).isEmpty();
assertThat(values).isEmpty();
}

@Test
void shouldReturnEmptyMapIfValuesCouldNotBeFoundInManifest_UsingClassLoader() {
var value = KiwiJars.readValuesFromJarManifest(this.getClass().getClassLoader(), "foo");
var values = KiwiJars.readValuesFromJarManifest(this.getClass().getClassLoader(), "foo");

assertThat(value).isEmpty();
assertThat(values).isEmpty();
}

@Test
void shouldReturnEmptyMapIfValuesCouldNotBeFoundInManifest_UsingDefaultClassLoader() {
var value = KiwiJars.readValuesFromJarManifest("foo");
var values = KiwiJars.readValuesFromJarManifest("foo");

assertThat(value).isEmpty();
assertThat(values).isEmpty();
}

@SuppressWarnings("ConstantValue")
@Test
void shouldReturnEmptyMap_ForInvalidClassLoader() {
ClassLoader classLoader = null;
var values = KiwiJars.readValuesFromJarManifest(classLoader, "Main-Class");

assertThat(values).isEmpty();
}

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

assertThat(values).isEmpty();
}
}

@Nested
class ReadFirstManifestOrNull {

@ClearBoxTest
void shouldReturnNull_WhenUrlsIsEmpty() {
var manifest = KiwiJars.readFirstManifestOrNull(List.of());
assertThat(manifest).isNull();
}

@ClearBoxTest
void shouldReturnNull_WhenUrlIsInvalid() throws MalformedURLException {
var urls = List.of(URI.create("jar:file:/tmp/12345/jars/foo-1.0.0.jar!/META-INF/MANIFEST.MF").toURL());

var manifest = KiwiJars.readFirstManifestOrNull(urls);
assertThat(manifest).isNull();
}
}
}

0 comments on commit f2021aa

Please sign in to comment.