| /* |
| * 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); |
| } |
| } |
| } |
| |
| } |