/*
* #%L
* Nazgul Project: nazgul-core-resource-api
* %%
* Copyright (C) 2010 - 2017 jGuru Europe AB
* %%
* Licensed under the jGuru Europe AB license (the "License"), based
* on Apache License, Version 2.0; you may not use this file except
* in compliance with the License.
*
* You may obtain a copy of the License at
*
* http://www.jguru.se/licenses/jguruCorporateSourceLicense-2.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*
*/
package se.jguru.nazgul.core.resource.api.extractor;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Pattern;
/**
* Trivial utility which extracts resources from a (self-contained) JAR.
* Also provides utility methods to simplify finding JarFiles originating from URLs to resources placed within JARs.
*
* @author <a href="mailto:lj@jguru.se">Lennart Jörelid</a>, jGuru Europe AB
*/
public abstract class JarExtractor {
// Our Log
private static final Logger log = LoggerFactory.getLogger(JarExtractor.class);
/**
* Pattern matching all resource names.
*/
public static final Pattern ALL_RESOURCES = Pattern.compile(".*");
// Internal state
private static final String JAR_PATH_SEPARATOR = "!";
/**
* Acquires the JarFile for the supplied packageResourceURL, which should be pointing to a resource packaged
* within a JAR. The first/outermost protocol for the supplied packagedResourceURL must be "jar",
* as a normal JAR resource URL has the form {@code jar:file:/some/path/aJarFile.jar!/path/in/jar/aFile.txt},
* where the path part is operating system dependent. (For instance, the windows counterpart could start with
* something like {@code jar:file:/C:/some/path/aJarFile.jar!/path/in/jar/aFile.txt}).
*
* @param packagedResourceURL a non-null URL pointing to a resource packaged within a JAR.
* @return The JarFile for the JAR within which the packagedResourceURL is found.
* @throws java.lang.IllegalArgumentException if the supplied URL did not correspond to a resource packaged
* within a JAR.
*/
public static JarFile getJarFileFor(final URL packagedResourceURL) throws IllegalArgumentException {
// Check sanity
Validate.notNull(packagedResourceURL, "Cannot handle null packagedResourceURL argument.");
final String jarProtocol = packagedResourceURL.getProtocol();
Validate.isTrue("jar".equalsIgnoreCase(jarProtocol),
"packagedResourceURL must have a 'jar' protocol. (Found: " + jarProtocol + ")");
// Peel off all protocols, to find the path to the JarFile's File.
URL innermostURL = packagedResourceURL;
while (true) {
try {
innermostURL = new URL(innermostURL.getPath());
} catch (MalformedURLException e) {
// Expected
break;
}
}
final String path = innermostURL.getPath();
final int exclamationIndex = path.indexOf(JAR_PATH_SEPARATOR);
Validate.isTrue(exclamationIndex > 0, "Required JAR path separator [" + JAR_PATH_SEPARATOR
+ "] not found in URLs path [" + path + "], distilled from packagedResourceURL ["
+ packagedResourceURL.toString() + "].");
final File jarFileFile = new File(path.substring(0, exclamationIndex));
Validate.isTrue(jarFileFile.exists() && jarFileFile.isFile(), "Inconsistent JarFile ["
+ jarFileFile.getAbsolutePath() + "]: nonexistent or not a File, distilled from packagedResourceURL ["
+ packagedResourceURL.toString() + "].");
// All seems well.
try {
return new JarFile(jarFileFile);
} catch (IOException e) {
throw new IllegalArgumentException("Could not create a JarFile from File ["
+ jarFileFile.getAbsolutePath() + "]", e);
}
}
/**
* Retrieves the JarEntry name for the supplied packagedResourceURL. Typically,
* this value can be used to acquire a JarEntry for the
* <pre>
* <code>
* // Get the JarFile for a JAR-based URL
* final JarFile foundJarFile = JarExtractor.getJarFileFor(resourceInJarURL);
*
* // Now, find the JarEntry name for the given resourceInJarURL
* final String name = JarExtractor.getEntryNameFor(resourceInJarURL);
*
* // Get the JarEntry for the resourceInJarURL
* final JarEntry entry = foundJarFile.getJarEntry(name);
* </code>
* </pre>
*
* @param packagedResourceURL A JAR URL, which must contain an '!' char. Typically on a form similar to
* {@code jar:file:/some/path/aJarFile.jar!/path/in/jar/aFile.txt}
* @return The entry name of the URL, which is the part following the '!/' char. Note that the first '/' must be
* peeled off to retrieve a valid JarEntry name.
*/
public static String getEntryNameFor(final URL packagedResourceURL) {
// Check sanity
Validate.notNull(packagedResourceURL, "Cannot handle null packagedResourceURL argument.");
// Find the path for the supplied packagedResourceURL
final String urlString = packagedResourceURL.toString();
final String tmp = urlString.substring(urlString.indexOf(JAR_PATH_SEPARATOR) + 1);
// Peel off the initial '/' to make a valid name.
Validate.isTrue(tmp.startsWith("/"), "The absolute path within the JAR should start with a '/' char. Got ["
+ tmp + "]");
return tmp.substring(1);
}
/**
* Extracts all resources whose names matches the supplied resourceIdentifier from jarFile
* to the targetDirectory. If targetDirectory does not exist and the createTargetDirectoryIfNonexistent
* parameter is {@code true}, the directory (and any parent directories) will be created.
*
* @param jarFile The JAR file from which some resources should be extracted.
* @param resourceIdentifier A Pattern matching the names of all resources which should be extracted.
* @param targetDirectory The directory to which all resources should be extracted.
* @param createTargetDirectoryIfNonexistent if {@code true}, the targetDirectory will be
* created if it does not already exist.
*/
public static void extractResourcesFrom(final JarFile jarFile,
final Pattern resourceIdentifier,
final File targetDirectory,
final boolean createTargetDirectoryIfNonexistent) {
// Check sanity
Validate.notNull(jarFile, "Cannot handle null jarFile argument.");
Validate.notNull(resourceIdentifier, "Cannot handle null resourceIdentifier argument.");
Validate.notNull(targetDirectory, "Cannot handle null targetDirectory argument.");
if (targetDirectory.exists() && !targetDirectory.isDirectory()) {
throw new IllegalArgumentException("Target [" + targetDirectory.getAbsolutePath()
+ "] exists and is not a directory.");
}
if (!createTargetDirectoryIfNonexistent && !targetDirectory.exists()) {
throw new IllegalArgumentException("Target directory [" + targetDirectory.getAbsolutePath()
+ "] does not exist - and instructed not to create it.");
}
// Do we need to create targetDir?
if (!targetDirectory.exists()) {
if (!targetDirectory.mkdirs()) {
throw new IllegalStateException("Could not create directory ["
+ targetDirectory.getAbsolutePath() + "]");
} else {
targetDirectory.mkdirs();
}
}
for (Enumeration<JarEntry> en = jarFile.entries(); en.hasMoreElements(); ) {
// Dig out the entry and its internal path.
final JarEntry current = en.nextElement();
final String entryPath = current.getName();
if (!current.isDirectory() && resourceIdentifier.matcher(entryPath).matches()) {
// Extract the file at its relative path location.
final File toWrite = new File(targetDirectory, entryPath);
final File parentFile = toWrite.getParentFile();
if (!parentFile.exists()) {
parentFile.mkdirs();
}
log.debug("Extracting [" + current.getName() + "] to [" + toWrite.getAbsolutePath() + "]");
InputStream inStream = null;
ReadableByteChannel inChannel = null;
FileOutputStream outStream = null;
FileChannel outChannel = null;
try {
try {
// Copy using a NIO channel to improve performance.
inStream = jarFile.getInputStream(current);
inChannel = Channels.newChannel(inStream);
outStream = new FileOutputStream(toWrite);
outChannel = outStream.getChannel();
outChannel.transferFrom(inChannel, 0, current.getSize());
} finally {
// Close all opened NIO objects
if (inStream != null) {
inStream.close();
}
if (inChannel != null) {
inChannel.close();
}
if (outStream != null) {
outStream.close();
}
if (outChannel != null) {
outChannel.close();
}
}
} catch (IOException e) {
throw new IllegalStateException("Could not create copy [" + entryPath + "] to ["
+ targetDirectory.getAbsolutePath() + "]", e);
}
}
}
}
}