Skip to content

Commit

Permalink
[Fix #45] Fix JarFile extraction utility (#46)
Browse files Browse the repository at this point in the history
  • Loading branch information
ksclarke committed Sep 24, 2022
1 parent c475822 commit 3c5326f
Show file tree
Hide file tree
Showing 11 changed files with 277 additions and 33 deletions.
3 changes: 2 additions & 1 deletion src/main/java/info/freelibrary/util/I18nObject.java
Expand Up @@ -46,7 +46,8 @@ public I18nObject(final String aBundleName) {
* @param aLocale The locale of the desired bundle.
*/
public I18nObject(final String aBundleName, final Locale aLocale) {
myBundle = (I18nResourceBundle) ResourceBundle.getBundle(aBundleName.toLowerCase(aLocale));
myBundle = (I18nResourceBundle) ResourceBundle.getBundle(aBundleName.toLowerCase(aLocale),
new CustomBundleControl());
}

/**
Expand Down
101 changes: 75 additions & 26 deletions src/main/java/info/freelibrary/util/JarUtils.java
Expand Up @@ -5,10 +5,9 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.LinkedList;
Expand All @@ -24,15 +23,25 @@
*/
public final class JarUtils {

/**
* A constant for the protocol of a Jar file.
*/
public static final String JAR_URL_PROTOCOL = "jar:file://";

/**
* The delimiter between the Jar file and its path.
*/
public static final String JAR_URL_DELIMITER = "!/";

/**
* The logger used by the Jar utilities.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(JarUtils.class, MessageCodes.BUNDLE);

/**
* A constant for the protocol of a Jar file.
* The number of components in a Jar URL.
*/
private static final String JAR_URL_PROTOCOL = "jar:file://";
private static final int URL_COMPONENT_COUNT = 2;

/**
* Creates a new Jar utilities instance.
Expand All @@ -54,7 +63,7 @@ public static URL[] getJarURLs() throws IOException {
for (final JarFile jarFile : ClasspathUtils.getJarFiles()) {
try (jarFile) {
final Manifest manifest = jarFile.getManifest();
final URL jarURL = new URL(JAR_URL_PROTOCOL + jarFile.getName() + "!/");
final URL jarURL = new URL(JAR_URL_PROTOCOL + jarFile.getName() + JAR_URL_DELIMITER);

urlList.add(jarURL);

Expand Down Expand Up @@ -84,53 +93,74 @@ public static URL[] getJarURLs() throws IOException {
}

/**
* Extract a particular path from a supplied Jar file to a supplied {@link java.io.File} location.
* Extracts the supplied path from the supplied Jar file's URL to the supplied {@link java.io.File} location. This
* method closes the JarFile.
*
* @param aJarFilePath A path to a Jar file from which to extract
* @param aFilePath The Jar file path of the file to extract
* @param aJarURL A Jar URL that contains the system file path of a Jar file and a subfile path
* @param aDestDir The destination directory into which the file should be extracted
* @throws IOException If there is an exception thrown while reading or writing the file
* @throws NoSuchFileException If the Jar file cannot be found at the supplied path
* @throws MalformedUrlException If the supplied URL does not also contain a subfile path
*/
public static void extract(final String aJarFilePath, final String aFilePath, final File aDestDir)
throws IOException {
File file;

try {
// Opening the URL connection just parses location, it doesn't really "open" in the I/O sense
final JarURLConnection connection = (JarURLConnection) new URL(aJarFilePath).openConnection();
public static void extract(final URL aJarURL, final File aDestDir) throws IOException {
final String[] jarURLParts = aJarURL.getFile().split(JAR_URL_DELIMITER);
final File jarFile;

file = new File(connection.getJarFileURL().getFile());
} catch (final MalformedURLException details) {
file = new File(aJarFilePath);
if (jarURLParts.length != URL_COMPONENT_COUNT) {
throw new MalformedUrlException(LOGGER.getMessage(MessageCodes.UTIL_072, aJarURL));
}

extract(file, aFilePath, aDestDir);
jarFile = new File(jarURLParts[0].substring(aJarURL.getProtocol().length() + 4)); // Remove protocol
extract(jarFile, jarURLParts[1], aDestDir);
}

/**
* Extract the supplied path from the supplied Jar file to a supplied {@link java.io.File} location. This method
* closes the JarFile.
*
* @param aJarFilePath The path to a Jar file from which to extract
* @param aFilePath The Jar's file path of the file to extract
* @param aDestDir The destination directory into which the file should be extracted
* @throws IOException If there is an exception thrown while reading or writing the file
* @throws NoSuchFileException If the Jar file cannot be found at the supplied path
*/
public static void extract(final String aJarFilePath, final String aFilePath, final File aDestDir)
throws IOException {
if (aJarFilePath.startsWith(JAR_URL_PROTOCOL)) {
extract(new URL(aJarFilePath + JAR_URL_DELIMITER + aFilePath), aDestDir);
} else {
extract(new File(aJarFilePath), aFilePath, aDestDir);
}
}

/**
* Extract a particular path from a supplied Jar file to a supplied {@link java.io.File} location.
* Extract the supplied path from the supplied Jar file to the supplied {@link java.io.File} location. This method
* closes the JarFile.
*
* @param aJarFile A Jar file from which to extract
* @param aFilePath The Jar file path of the file to extract
* @param aFilePath The Jar's file path of the file to extract
* @param aDestDir The destination directory into which the file should be extracted
* @throws IOException If there is an exception thrown while reading or writing the file
* @throws NoSuchFileException If the Jar file cannot be found at the supplied path
*/
public static void extract(final File aJarFile, final String aFilePath, final File aDestDir) throws IOException {
extract(new JarFile(aJarFile), aFilePath, aDestDir);
}

/**
* Extract a particular path from a supplied Jar file to a supplied {@link java.io.File} location.
* Extract a particular path from the supplied Jar file to a supplied {@link java.io.File} location. This method
* closes the JarFile.
*
* @param aJarFile A Jar file from which to extract
* @param aFilePath The Jar file path of the file to extract
* @param aJarFile A Jar file from which to extract the supplied file path
* @param aFilePath The Jar's file path of the file to extract
* @param aDestDir The destination directory into which the file should be extracted
* @throws IOException If there is an exception thrown while reading or writing the file
* @throws NoSuchFileException If the Jar file cannot be found at the supplied path
*/
public static void extract(final JarFile aJarFile, final String aFilePath, final File aDestDir) throws IOException {
final Enumeration<JarEntry> entries = aJarFile.entries();

try (aJarFile) {
final Enumeration<JarEntry> entries = aJarFile.entries();

while (entries.hasMoreElements()) {
final JarEntry entry = entries.nextElement();
final String entryName = entry.getName();
Expand All @@ -149,6 +179,25 @@ public static void extract(final JarFile aJarFile, final String aFilePath, final
}
}
}
} // Close JarFile when done with it
}

/**
* Tests whether the supplied file path exists in the supplied Jar file. This method does not close the JarFile.
*
* @param aJarFile A jar file to search
* @param aFilePath A path for which to search
* @return True if the file path is found; else, false
*/
public static boolean contains(final JarFile aJarFile, final String aFilePath) throws IOException {
final Enumeration<JarEntry> entries = aJarFile.entries();

while (entries.hasMoreElements()) {
if (entries.nextElement().getName().equals(aFilePath)) {
return true;
}
}

return false;
}
}
32 changes: 32 additions & 0 deletions src/main/java/info/freelibrary/util/LoggingConsumer.java
@@ -0,0 +1,32 @@

package info.freelibrary.util;

import java.util.function.Consumer;

/**
* A consumer that logs any exceptions. This is not an ideal solution (as the PMD suppressions indicate), but it
* provides a generic way to handle exceptions thrown in lambdas.
*
* @param <T> The type that the function accepts
*/
@FunctionalInterface
public interface LoggingConsumer<T> extends Consumer<T> {

@Override
@SuppressWarnings("PMD.AvoidCatchingGenericException")
default void accept(final T aType) {
try {
acceptLogs(aType);
} catch (final Exception details) {
LoggerFactory.getLogger(LoggingConsumer.class, MessageCodes.BUNDLE).error(details, details.getMessage());
}
}

/**
* A method that logs any exceptions thrown by the consumer.
*
* @param aType A type being accepted by the consumer
*/
void acceptLogs(T aType);

}
34 changes: 34 additions & 0 deletions src/main/java/info/freelibrary/util/ThrowingConsumer.java
@@ -0,0 +1,34 @@

package info.freelibrary.util;

import java.util.function.Consumer;

/**
* A consumer that throws a runtime exception. This is not an ideal solution (as the PMD suppressions indicate), but it
* provides a generic way to handle exceptions thrown in lambdas.
*
* @param <T> The type that the function accepts
*/
@FunctionalInterface
public interface ThrowingConsumer<T> extends Consumer<T> {

@Override
@SuppressWarnings("PMD.AvoidCatchingGenericException")
default void accept(final T aType) {
try {
acceptThrows(aType);
} catch (final Exception details) {
throw new I18nRuntimeException(details);
}
}

/**
* A method that wraps any exceptions through by the consumer with a runtime exception.
*
* @param aType A type being accepted by the consumer
* @throws Exception An exception thrown by the consumer
*/
@SuppressWarnings("PMD.SignatureDeclareThrowsException")
void acceptThrows(T aType) throws Exception;

}
30 changes: 30 additions & 0 deletions src/main/java/info/freelibrary/util/warnings/PMD.java
Expand Up @@ -88,6 +88,36 @@ public final class PMD {
*/
public static final String UNUSED_PRIVATE_METHOD = "PMD.UnusedPrivateMethod";

/**
* Cf. https://pmd.github.io/latest/pmd_rules_java_design.html#cognitivecomplexity
*/
public static final String COGNITIVE_COMPLEXITY = "PMD.CognitiveComplexity";

/**
* Cf. https://pmd.github.io/latest/pmd_rules_java_performance.html#avoidfilestream
*/
public static final String AVOID_FILE_STREAM = "PMD.AvoidFileStream";

/**
* Cf. https://pmd.github.io/latest/pmd_rules_java_errorprone.html#avoidliteralsinifcondition
*/
public static final String AVOID_LITERALS_IN_IF_CONDITION = "PMD.AvoidLiteralsInIfCondition";

/**
* Cf. https://pmd.github.io/latest/pmd_rules_java_errorprone.html#morethanonelogger
*/
public static final String MORE_THAN_ONE_LOGGER = "PMD.MoreThanOneLogger";

/**
* Cf. https://pmd.github.io/latest/pmd_rules_java_performance.html#consecutiveliteralappends
*/
public static final String CONSECUTIVE_LITERAL_APPENDS = "PMD.ConsecutiveLiteralAppends";

/**
* Cf. https://pmd.github.io/latest/pmd_rules_java_errorprone.html#avoidduplicateliterals
*/
public static final String AVOID_DUPLICATE_LITERALS = "PMD.AvoidDuplicateLiterals";

/*
* Constant classes have private constructors.
*/
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/freelib-utils_messages.xml
Expand Up @@ -81,5 +81,6 @@
<entry key="UTIL-069">The start index position ({}) is after the end position ({})</entry>
<entry key="UTIL-070">Start position out of bounds: {}</entry>
<entry key="UTIL-071">End position out of bounds: {}</entry>
<entry key="UTIL-072">Jar file's URL must contain a subfile path too: {}</entry>

</properties>
12 changes: 11 additions & 1 deletion src/test/java/info/freelibrary/util/I18nObjectTest.java
@@ -1,7 +1,9 @@

package info.freelibrary.util;

import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.File;
import java.io.IOException;
Expand Down Expand Up @@ -165,4 +167,12 @@ public void testHasI18nKey() {
assertTrue(i18nObj.hasI18nKey(TEST_ONE));
assertEquals(ONE, i18nObj.getI18n(TEST_ONE));
}

/**
* Tests the use of an I18n properties file.
*/
@Test
public void testPropertiesFile() {
assertEquals(1, new I18nObjectWrapper("test_messages").countKeys());
}
}

0 comments on commit 3c5326f

Please sign in to comment.