blob: b96c85f7ad3b1c3eb3e33981e2844813ebf4ecbf [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
package freemarker.test;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.io.Files;
import freemarker.template.utility.StringUtil;
/**
* Extracts a collection of resources from a Java package into a file system directory, based on the {@value ResourcesExtractor#CONTENTS_TXT}
* resource in it. It won't scan to find resources automatically, so everything has to be added to this file.
*
* <p>Regarding the content of {@value ResourcesExtractor#CONTENTS_TXT}:
* <ul>
* <li>Lines starting with {code //} or {code /*} or {code *} are comments.
* <li>Each non-empty line in the {@value ResourcesExtractor#CONTENTS_TXT} that isn't comment, there must be
* a resource path relative to the "directory" that contains {@value ResourcesExtractor#CONTENTS_TXT}. Only the
* files referred this way will be copied. (Referring to directories doesn't copy the files in them.)
* <li>If the entry ends with {@code "/"}, then it denotes a directory that must be created even if there will be
* no files in it. Otherwise it's redundant to add such entries.
* <li>The content of "subdirectories" whose name ends with {@value ResourcesExtractor#SUFFIX_JAR} will be copied
* into a jar file with similar name, instead of into a directory.
* <li>An line may contains {@value #ARROW}, in which case the left side of the {@value #ARROW} is the
* <em>absolute</em> path of the copied resource, and the right side of the {@value #ARROW} is the target
* relative path (like the path in the usual lines). This is useful for copying class files generated by
* the normal Java compilation mechanism into target subdirectories like {@code WEB-INF\classes\com\example}.
* </ul>
*/
public final class ResourcesExtractor {
private static final String ARROW = "->";
private static final String DOT_TMP = ".tmp";
public static final String CONTENTS_TXT = "CONTENTS.txt";
public static final String SUFFIX_JAR = ".jar";
private static final Logger LOG = LoggerFactory.getLogger(ResourcesExtractor.class);
private ResourcesExtractor() {
// Not meant to be instantiated
}
/**
* @param resolverClass
* The class with which the resources are loaded.
* @param srcDirResourcePath
* The resource path to which the paths in {@value ResourcesExtractor#CONTENTS_TXT} are relative to. If
* the value of this parameter doesn't start with {@code "/"}, then it's relative to the package of the
* {@code resolverClass}.
*
* @return The temporary directory into which the resource where extracted to. Don't forget to delete it when it's
* not used anymore.
*/
public static File extract(Class resolverClass, String srcDirResourcePath, File dstRootDir) throws IOException {
if (!srcDirResourcePath.endsWith("/")) {
srcDirResourcePath += "/";
}
String contResource = srcDirResourcePath + CONTENTS_TXT;
InputStream contIn = resolverClass.getResourceAsStream(contResource);
if (contIn == null) {
throw new IOException("Can't find resource: class=" + resolverClass + ", path=" + contResource);
}
boolean deleteDstRootDir;
if (dstRootDir == null) {
dstRootDir = Files.createTempDir();
deleteDstRootDir = true;
} else {
deleteDstRootDir = !dstRootDir.exists();
}
try {
try (BufferedReader contR = new BufferedReader(new InputStreamReader(contIn, "UTF-8"))) {
String contLine;
while ((contLine = contR.readLine()) != null) {
processLine(contLine, resolverClass, srcDirResourcePath, dstRootDir, contResource);
}
}
jarMarkedSubdirectories(dstRootDir);
deleteDstRootDir = false;
} finally {
if (deleteDstRootDir) {
try {
if (dstRootDir.getParentFile() == null) {
throw new IOException("Won't delete the root directory");
}
FileUtils.deleteDirectory(dstRootDir);
} catch (IOException e) {
LOG.error("Failed to delete destination directory: " + dstRootDir, e);
}
}
}
return dstRootDir;
}
private static void processLine(String contLine, Class<?> resolverClass, String srcDirResourcePath, File dstRootDir,
String contResource) throws IOException {
contLine = contLine.trim();
if (contLine.isEmpty() || contLine.startsWith("//") || contLine.startsWith("/*")
|| contLine.startsWith("*")) {
return;
}
String contSrcPath = contLine;
String contDstPath = contLine;
boolean contSrcPathRelative;
int arrowIdx = contLine.indexOf(ARROW);
if (arrowIdx != -1) {
if (!contLine.startsWith("/")) {
throw new IOException("In " + StringUtil.jQuote(contResource) + ", this line must start with "
+ "\"/\" as it uses the " + StringUtil.jQuote(ARROW) + " operator : "
+ contLine);
}
contSrcPath = contLine.substring(0, arrowIdx).trim();
contDstPath = contLine.substring(arrowIdx + ARROW.length()).trim();
contSrcPathRelative = false;
} else {
if (contLine.startsWith("/")) {
throw new IOException("In " + StringUtil.jQuote(contResource)
+ ", this line can't start with \"/\": " + contLine);
}
contSrcPathRelative = true;
contSrcPath = contLine;
contDstPath = contLine;
}
File dstFile = new File(dstRootDir, contDstPath);
if (contLine.endsWith("/")) {
if (!dstFile.mkdirs()) {
throw new IOException("Failed to create directory: " + dstFile);
}
} else {
String srcEntryPath = contSrcPathRelative ? srcDirResourcePath + contSrcPath : contSrcPath;
InputStream entryIn = resolverClass.getResourceAsStream(srcEntryPath);
if (entryIn == null) {
throw new IOException("Can't find resource: class=" + resolverClass + ", path=" + srcEntryPath);
}
try {
if (dstFile.exists()) {
throw new IOException(
"Destination already exists; check if " + StringUtil.jQuote(contDstPath)
+ " occurs for multiple times in \"" + CONTENTS_TXT + "\".");
}
FileUtils.copyInputStreamToFile(entryIn, dstFile);
} catch (IOException e) {
File parent = dstFile;
while ((parent = dstFile.getParentFile()) != null) {
if (parent.isFile()) {
throw new IOException("An ancestor directory of " + StringUtil.jQuote(dstFile) + ", "
+ StringUtil.jQuote(parent) + " already exists, but as a file, not as a directory. "
+ "Check if you have accidentally added the directory itself to \"" + CONTENTS_TXT
+ "\". Only files should be listed there.");
}
}
throw e;
} finally {
entryIn.close();
}
}
}
/**
* @param extension
* The file extension of the resulting jar archive, or {@code null} if the archive name will be the same
* as the directory name.
*/
private static File replaceDirectoryWithJar(File srcDir, String extension) throws IOException {
String workJarFileName;
String finalJarFileName;
if (extension == null) {
finalJarFileName = srcDir.getName();
workJarFileName = finalJarFileName + DOT_TMP;
} else {
finalJarFileName = srcDir.getName() + "." + extension;
workJarFileName = finalJarFileName;
}
File workJarFile = new File(srcDir.getParentFile(), workJarFileName);
jarDirectory(srcDir, workJarFile);
if (srcDir.getParentFile() == null) {
throw new IOException("Won't delete the root directory");
}
FileUtils.deleteDirectory(srcDir);
File finalJarFile;
if (!workJarFileName.equals(finalJarFileName)) {
finalJarFile = new File(workJarFile.getParentFile(), finalJarFileName);
FileUtils.moveFile(workJarFile, finalJarFile);
} else {
finalJarFile = workJarFile;
}
return finalJarFile;
}
private static void jarDirectory(File srcDir, File jarFile) throws FileNotFoundException, IOException {
boolean finished = false;
try {
try (FileOutputStream fileOut = new FileOutputStream(jarFile)) {
JarOutputStream jarOut = new JarOutputStream(fileOut);
try {
addFilesToJar("", srcDir, jarOut);
} finally {
jarOut.close();
}
}
finished = true;
} finally {
if (!finished) {
if (!jarFile.delete()) {
LOG.error("Failed to delete file: {}", jarFile);
}
}
}
}
private static void jarMarkedSubdirectories(File dir) throws IOException {
File[] entries = dir.listFiles();
if (entries == null) {
throw new IOException("Failed to list directory: " + dir);
}
for (File entry : entries) {
if (entry.isDirectory()) {
jarMarkedSubdirectories(entry);
if (entry.getName().endsWith(SUFFIX_JAR)) {
replaceDirectoryWithJar(entry, null);
}
}
}
}
private static void addFilesToJar(String entryBasePath, File dir, JarOutputStream jarOut) throws IOException {
File[] entries = dir.listFiles();
if (entries == null) {
throw new IOException("Couldn't list directory: " + dir);
}
for (File entry : entries) {
if (entry.isFile()) {
jarOut.putNextEntry(new ZipEntry(entryBasePath + entry.getName()));
try (FileInputStream fileIn = new FileInputStream(entry)) {
IOUtils.copy(fileIn, jarOut);
}
jarOut.closeEntry();
} else if (entry.isDirectory()) {
String dirPath = entryBasePath + entry.getName() + "/";
jarOut.putNextEntry(new ZipEntry(dirPath));
jarOut.closeEntry();
addFilesToJar(dirPath, entry, jarOut);
} else {
throw new IOException("Couldn't open source entry: " + entry);
}
}
}
}