/* * 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 org.apache.tools.ant.util; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Reader; import java.nio.channels.FileChannel; import java.nio.charset.Charset; import java.nio.file.StandardOpenOption; import java.util.Arrays; import java.util.Vector; import org.apache.tools.ant.Project; import org.apache.tools.ant.ProjectComponent; import org.apache.tools.ant.filters.util.ChainReaderHelper; import org.apache.tools.ant.types.FilterChain; import org.apache.tools.ant.types.FilterSetCollection; import org.apache.tools.ant.types.Resource; import org.apache.tools.ant.types.ResourceCollection; import org.apache.tools.ant.types.ResourceFactory; import org.apache.tools.ant.types.TimeComparison; import org.apache.tools.ant.types.resources.Appendable; import org.apache.tools.ant.types.resources.FileProvider; import org.apache.tools.ant.types.resources.FileResource; import org.apache.tools.ant.types.resources.Resources; import org.apache.tools.ant.types.resources.Restrict; import org.apache.tools.ant.types.resources.StringResource; import org.apache.tools.ant.types.resources.Touchable; import org.apache.tools.ant.types.resources.Union; import org.apache.tools.ant.types.resources.selectors.Date; import org.apache.tools.ant.types.resources.selectors.ResourceSelector; import org.apache.tools.ant.types.selectors.SelectorUtils; // CheckStyle:HideUtilityClassConstructorCheck OFF - bc /** * This class provides utility methods to process Resources. * * @since Ant 1.5.2 */ public class ResourceUtils { /** Utilities used for file operations */ private static final FileUtils FILE_UTILS = FileUtils.getFileUtils(); /** * Name of charset "ISO Latin Alphabet No. 1, a.k.a. ISO-LATIN-1". * * @since Ant 1.8.1 */ public static final String ISO_8859_1 = "ISO-8859-1"; private static final long MAX_IO_CHUNK_SIZE = 16*1024*1024l; // 16 MB /** * Tells which source files should be reprocessed based on the * last modification date of target files. * @param logTo where to send (more or less) interesting output. * @param source array of resources bearing relative path and last * modification date. * @param mapper filename mapper indicating how to find the target * files. * @param targets object able to map as a resource a relative path * at <b>destination</b>. * @return array containing the source files which need to be * copied or processed, because the targets are out of date or do * not exist. */ public static Resource[] selectOutOfDateSources(final ProjectComponent logTo, final Resource[] source, final FileNameMapper mapper, final ResourceFactory targets) { return selectOutOfDateSources(logTo, source, mapper, targets, FILE_UTILS.getFileTimestampGranularity()); } /** * Tells which source files should be reprocessed based on the * last modification date of target files. * @param logTo where to send (more or less) interesting output. * @param source array of resources bearing relative path and last * modification date. * @param mapper filename mapper indicating how to find the target * files. * @param targets object able to map as a resource a relative path * at <b>destination</b>. * @param granularity The number of milliseconds leeway to give * before deciding a target is out of date. * @return array containing the source files which need to be * copied or processed, because the targets are out of date or do * not exist. * @since Ant 1.6.2 */ public static Resource[] selectOutOfDateSources(final ProjectComponent logTo, final Resource[] source, final FileNameMapper mapper, final ResourceFactory targets, final long granularity) { final Union u = new Union(); u.addAll(Arrays.asList(source)); final ResourceCollection rc = selectOutOfDateSources(logTo, u, mapper, targets, granularity); return rc.size() == 0 ? new Resource[0] : ((Union) rc).listResources(); } /** * Tells which sources should be reprocessed based on the * last modification date of targets. * @param logTo where to send (more or less) interesting output. * @param source ResourceCollection. * @param mapper filename mapper indicating how to find the target Resources. * @param targets object able to map a relative path as a Resource. * @param granularity The number of milliseconds leeway to give * before deciding a target is out of date. * @return ResourceCollection. * @since Ant 1.7 */ public static ResourceCollection selectOutOfDateSources(final ProjectComponent logTo, final ResourceCollection source, final FileNameMapper mapper, final ResourceFactory targets, final long granularity) { logFuture(logTo, source, granularity); return selectSources(logTo, source, mapper, targets, sr -> target -> SelectorUtils.isOutOfDate(sr, target, granularity)); } /** * Tells which sources should be reprocessed because the given * selector selects at least one target. * * @param logTo where to send (more or less) interesting output. * @param source ResourceCollection. * @param mapper filename mapper indicating how to find the target Resources. * @param targets object able to map a relative path as a Resource. * @param selector returns a selector that is applied to target * files. If it selects at least one target the source will be * added to the returned collection. * @return ResourceCollection. * @since Ant 1.8.0 */ public static ResourceCollection selectSources(final ProjectComponent logTo, ResourceCollection source, final FileNameMapper mapper, final ResourceFactory targets, final ResourceSelectorProvider selector) { if (source.isEmpty()) { logTo.log("No sources found.", Project.MSG_VERBOSE); return Resources.NONE; } source = Union.getInstance(source); final Union result = new Union(); for (final Resource sr : source) { String srName = sr.getName(); srName = srName == null ? srName : srName.replace('/', File.separatorChar); String[] targetnames = null; try { targetnames = mapper.mapFileName(srName); } catch (final Exception e) { logTo.log("Caught " + e + " mapping resource " + sr, Project.MSG_VERBOSE); } if (targetnames == null || targetnames.length == 0) { logTo.log(sr + " skipped - don\'t know how to handle it", Project.MSG_VERBOSE); continue; } for (int i = 0; i < targetnames.length; i++) { if (targetnames[i] == null) { targetnames[i] = "(no name)"; } } final Union targetColl = new Union(); for (int i = 0; i < targetnames.length; i++) { targetColl.add(targets.getResource( targetnames[i].replace(File.separatorChar, '/'))); } //find the out-of-date targets: final Restrict r = new Restrict(); r.add(selector.getTargetSelectorForSource(sr)); r.add(targetColl); if (r.size() > 0) { result.add(sr); final Resource t = r.iterator().next(); logTo.log(sr.getName() + " added as " + t.getName() + (t.isExists() ? " is outdated." : " doesn\'t exist."), Project.MSG_VERBOSE); continue; } //log uptodateness of all targets: logTo.log(sr.getName() + " omitted as " + targetColl.toString() + (targetColl.size() == 1 ? " is" : " are ") + " up to date.", Project.MSG_VERBOSE); } return result; } /** * Convenience method to copy content from one Resource to another. * No filtering is performed. * * @param source the Resource to copy from. * Must not be <code>null</code>. * @param dest the Resource to copy to. * Must not be <code>null</code>. * * @throws IOException if the copying fails. * * @since Ant 1.7 */ public static void copyResource(final Resource source, final Resource dest) throws IOException { copyResource(source, dest, null); } /** * Convenience method to copy content from one Resource to another. * No filtering is performed. * * @param source the Resource to copy from. * Must not be <code>null</code>. * @param dest the Resource to copy to. * Must not be <code>null</code>. * @param project the project instance. * * @throws IOException if the copying fails. * * @since Ant 1.7 */ public static void copyResource(final Resource source, final Resource dest, final Project project) throws IOException { copyResource(source, dest, null, null, false, false, null, null, project); } // CheckStyle:ParameterNumberCheck OFF - bc /** * Convenience method to copy content from one Resource to another * specifying whether token filtering must be used, whether filter chains * must be used, whether newer destination files may be overwritten and * whether the last modified time of <code>dest</code> file should be made * equal to the last modified time of <code>source</code>. * * @param source the Resource to copy from. * Must not be <code>null</code>. * @param dest the Resource to copy to. * Must not be <code>null</code>. * @param filters the collection of filters to apply to this copy. * @param filterChains filterChains to apply during the copy. * @param overwrite Whether or not the destination Resource should be * overwritten if it already exists. * @param preserveLastModified Whether or not the last modified time of * the destination Resource should be set to that * of the source. * @param inputEncoding the encoding used to read the files. * @param outputEncoding the encoding used to write the files. * @param project the project instance. * * @throws IOException if the copying fails. * * @since Ant 1.7 */ public static void copyResource(final Resource source, final Resource dest, final FilterSetCollection filters, final Vector<FilterChain> filterChains, final boolean overwrite, final boolean preserveLastModified, final String inputEncoding, final String outputEncoding, final Project project) throws IOException { copyResource(source, dest, filters, filterChains, overwrite, preserveLastModified, false, inputEncoding, outputEncoding, project); } // CheckStyle:ParameterNumberCheck OFF - bc /** * Convenience method to copy content from one Resource to another * specifying whether token filtering must be used, whether filter chains * must be used, whether newer destination files may be overwritten and * whether the last modified time of <code>dest</code> file should be made * equal to the last modified time of <code>source</code>. * * @param source the Resource to copy from. * Must not be <code>null</code>. * @param dest the Resource to copy to. * Must not be <code>null</code>. * @param filters the collection of filters to apply to this copy. * @param filterChains filterChains to apply during the copy. * @param overwrite Whether or not the destination Resource should be * overwritten if it already exists. * @param preserveLastModified Whether or not the last modified time of * the destination Resource should be set to that * of the source. * @param append Whether to append to an Appendable Resource. * @param inputEncoding the encoding used to read the files. * @param outputEncoding the encoding used to write the files. * @param project the project instance. * * @throws IOException if the copying fails. * * @since Ant 1.8 */ public static void copyResource(final Resource source, final Resource dest, final FilterSetCollection filters, final Vector<FilterChain> filterChains, final boolean overwrite, final boolean preserveLastModified, final boolean append, final String inputEncoding, final String outputEncoding, final Project project) throws IOException { copyResource(source, dest, filters, filterChains, overwrite, preserveLastModified, append, inputEncoding, outputEncoding, project, /* force: */ false); } /** * Convenience method to copy content from one Resource to another * specifying whether token filtering must be used, whether filter chains * must be used, whether newer destination files may be overwritten and * whether the last modified time of <code>dest</code> file should be made * equal to the last modified time of <code>source</code>. * * @param source the Resource to copy from. * Must not be <code>null</code>. * @param dest the Resource to copy to. * Must not be <code>null</code>. * @param filters the collection of filters to apply to this copy. * @param filterChains filterChains to apply during the copy. * @param overwrite Whether or not the destination Resource should be * overwritten if it already exists. * @param preserveLastModified Whether or not the last modified time of * the destination Resource should be set to that * of the source. * @param append Whether to append to an Appendable Resource. * @param inputEncoding the encoding used to read the files. * @param outputEncoding the encoding used to write the files. * @param project the project instance. * @param force whether read-only target files will be overwritten * * @throws IOException if the copying fails. * * @since Ant 1.8.2 */ public static void copyResource(final Resource source, final Resource dest, final FilterSetCollection filters, final Vector<FilterChain> filterChains, final boolean overwrite, final boolean preserveLastModified, final boolean append, final String inputEncoding, final String outputEncoding, final Project project, final boolean force) throws IOException { if (!(overwrite || SelectorUtils.isOutOfDate(source, dest, FileUtils.getFileUtils() .getFileTimestampGranularity()))) { return; } final boolean filterSetsAvailable = (filters != null && filters.hasFilters()); final boolean filterChainsAvailable = (filterChains != null && !filterChains.isEmpty()); String effectiveInputEncoding; if (source instanceof StringResource) { effectiveInputEncoding = ((StringResource) source).getEncoding(); } else { effectiveInputEncoding = inputEncoding; } File destFile = null; if (dest.as(FileProvider.class) != null) { destFile = dest.as(FileProvider.class).getFile(); } if (destFile != null && destFile.isFile() && !destFile.canWrite()) { if (!force) { throw new ReadOnlyTargetFileException(destFile); } if (!FILE_UTILS.tryHardToDelete(destFile)) { throw new IOException( "failed to delete read-only destination file " + destFile); } } if (filterSetsAvailable) { copyWithFilterSets(source, dest, filters, filterChains, append, effectiveInputEncoding, outputEncoding, project); } else if (filterChainsAvailable || (effectiveInputEncoding != null && !effectiveInputEncoding.equals(outputEncoding)) || (effectiveInputEncoding == null && outputEncoding != null)) { copyWithFilterChainsOrTranscoding(source, dest, filterChains, append, effectiveInputEncoding, outputEncoding, project); } else { boolean copied = false; if (source.as(FileProvider.class) != null && destFile != null && !append) { final File sourceFile = source.as(FileProvider.class).getFile(); try { copyUsingFileChannels(sourceFile, destFile); copied = true; } catch (final IOException ex) { String msg = "Attempt to copy " + sourceFile + " to " + destFile + " using NIO Channels" + " failed due to '" + ex.getMessage() + "'. Falling back to streams."; if (project != null) { project.log(msg, Project.MSG_WARN); } else { System.err.println(msg); } } } if (!copied) { copyUsingStreams(source, dest, append, project); } } if (preserveLastModified) { final Touchable t = dest.as(Touchable.class); if (t != null) { setLastModified(t, source.getLastModified()); } } } // CheckStyle:ParameterNumberCheck ON /** * Set the last modified time of an object implementing * org.apache.tools.ant.types.resources.Touchable . * * @param t the Touchable whose modified time is to be set. * @param time the time to which the last modified time is to be set. * if this is -1, the current time is used. * @since Ant 1.7 */ public static void setLastModified(final Touchable t, final long time) { t.touch((time < 0) ? System.currentTimeMillis() : time); } /** * Compares the contents of two Resources. * * @param r1 the Resource whose content is to be compared. * @param r2 the other Resource whose content is to be compared. * @param text true if the content is to be treated as text and * differences in kind of line break are to be ignored. * * @return true if the content of the Resources is the same. * * @throws IOException if the Resources cannot be read. * @since Ant 1.7 */ public static boolean contentEquals(final Resource r1, final Resource r2, final boolean text) throws IOException { if (r1.isExists() != r2.isExists()) { return false; } if (!r1.isExists()) { // two not existing files are equal return true; } // should the following two be switched? If r1 and r2 refer to the same file, // isn't their content equal regardless of whether that file is a directory? if (r1.isDirectory() || r2.isDirectory()) { // don't want to compare directory contents for now return false; } if (r1.equals(r2)) { return true; } if (!text) { final long s1 = r1.getSize(); final long s2 = r2.getSize(); if (s1 != Resource.UNKNOWN_SIZE && s2 != Resource.UNKNOWN_SIZE && s1 != s2) { return false; } } return compareContent(r1, r2, text) == 0; } /** * Compare the content of two Resources. A nonexistent Resource's * content is "less than" that of an existing Resource; a directory-type * Resource's content is "less than" that of a file-type Resource. * @param r1 the Resource whose content is to be compared. * @param r2 the other Resource whose content is to be compared. * @param text true if the content is to be treated as text and * differences in kind of line break are to be ignored. * @return a negative integer, zero, or a positive integer as the first * argument is less than, equal to, or greater than the second. * @throws IOException if the Resources cannot be read. * @since Ant 1.7 */ public static int compareContent(final Resource r1, final Resource r2, final boolean text) throws IOException { if (r1.equals(r2)) { return 0; } final boolean e1 = r1.isExists(); final boolean e2 = r2.isExists(); if (!(e1 || e2)) { return 0; } if (e1 != e2) { return e1 ? 1 : -1; } final boolean d1 = r1.isDirectory(); final boolean d2 = r2.isDirectory(); if (d1 && d2) { return 0; } if (d1 || d2) { return d1 ? -1 : 1; } return text ? textCompare(r1, r2) : binaryCompare(r1, r2); } /** * Convenience method to turn any fileProvider into a basic * FileResource with the file's immediate parent as the basedir, * for tasks that need one. * @param fileProvider input * @return fileProvider if it is a FileResource instance, or a new * FileResource with fileProvider's file. * @since Ant 1.8 */ public static FileResource asFileResource(final FileProvider fileProvider) { if (fileProvider instanceof FileResource || fileProvider == null) { return (FileResource) fileProvider; } return new FileResource(Project.getProject(fileProvider), fileProvider.getFile()); } /** * Binary compares the contents of two Resources. * <p> * simple but sub-optimal comparison algorithm. written for working * rather than fast. Better would be a block read into buffers followed * by long comparisons apart from the final 1-7 bytes. * </p> * * @param r1 the Resource whose content is to be compared. * @param r2 the other Resource whose content is to be compared. * @return a negative integer, zero, or a positive integer as the first * argument is less than, equal to, or greater than the second. * @throws IOException if the Resources cannot be read. * @since Ant 1.7 */ private static int binaryCompare(final Resource r1, final Resource r2) throws IOException { try (InputStream in1 = new BufferedInputStream(r1.getInputStream()); InputStream in2 = new BufferedInputStream(r2.getInputStream())) { for (int b1 = in1.read(); b1 != -1; b1 = in1.read()) { final int b2 = in2.read(); if (b1 != b2) { return b1 > b2 ? 1 : -1; } } return in2.read() == -1 ? 0 : -1; } } /** * Text compares the contents of two Resources. * Ignores different kinds of line endings. * @param r1 the Resource whose content is to be compared. * @param r2 the other Resource whose content is to be compared. * @return a negative integer, zero, or a positive integer as the first * argument is less than, equal to, or greater than the second. * @throws IOException if the Resources cannot be read. * @since Ant 1.7 */ private static int textCompare(final Resource r1, final Resource r2) throws IOException { try (BufferedReader in1 = new BufferedReader(new InputStreamReader(r1.getInputStream())); BufferedReader in2 = new BufferedReader( new InputStreamReader(r2.getInputStream()))) { String expected = in1.readLine(); while (expected != null) { final String actual = in2.readLine(); if (!expected.equals(actual)) { if (actual == null) { return 1; } return expected.compareTo(actual); } expected = in1.readLine(); } return in2.readLine() == null ? 0 : -1; //NOSONAR } } /** * Log which Resources (if any) have been modified in the future. * @param logTo the ProjectComponent to do the logging. * @param rc the collection of Resources to check. * @param granularity the timestamp granularity to use. * @since Ant 1.7 */ private static void logFuture(final ProjectComponent logTo, final ResourceCollection rc, final long granularity) { final long now = System.currentTimeMillis() + granularity; final Date sel = new Date(); sel.setMillis(now); sel.setWhen(TimeComparison.AFTER); final Restrict future = new Restrict(); future.add(sel); future.add(rc); for (final Resource r : future) { logTo.log("Warning: " + r.getName() + " modified in the future.", Project.MSG_WARN); } } private static void copyWithFilterSets(final Resource source, final Resource dest, final FilterSetCollection filters, final Vector<FilterChain> filterChains, final boolean append, final String inputEncoding, final String outputEncoding, final Project project) throws IOException { try (Reader in = filterWith(project, inputEncoding, filterChains, source.getInputStream()); BufferedWriter out = new BufferedWriter(new OutputStreamWriter( getOutputStream(dest, append, project), charsetFor(outputEncoding)))) { final LineTokenizer lineTokenizer = new LineTokenizer(); lineTokenizer.setIncludeDelims(true); String newline = null; String line = lineTokenizer.getToken(in); while (line != null) { if (line.length() == 0) { // this should not happen, because the lines are // returned with the end of line delimiter out.newLine(); } else { newline = filters.replaceTokens(line); out.write(newline); } line = lineTokenizer.getToken(in); } } } private static Reader filterWith(Project project, String encoding, Vector<FilterChain> filterChains, InputStream input) { Reader r = new InputStreamReader(input, charsetFor(encoding)); if (filterChains != null && !filterChains.isEmpty()) { final ChainReaderHelper crh = new ChainReaderHelper(); crh.setBufferSize(FileUtils.BUF_SIZE); crh.setPrimaryReader(r); crh.setFilterChains(filterChains); crh.setProject(project); r = crh.getAssembledReader(); } return new BufferedReader(r); } private static Charset charsetFor(String encoding) { return encoding == null ? Charset.defaultCharset() : Charset.forName(encoding); } private static void copyWithFilterChainsOrTranscoding(final Resource source, final Resource dest, final Vector<FilterChain> filterChains, final boolean append, final String inputEncoding, final String outputEncoding, final Project project) throws IOException { try (Reader in = filterWith(project, inputEncoding, filterChains, source.getInputStream()); BufferedWriter out = new BufferedWriter(new OutputStreamWriter( getOutputStream(dest, append, project), charsetFor(outputEncoding)))) { final char[] buffer = new char[FileUtils.BUF_SIZE]; while (true) { final int nRead = in.read(buffer, 0, buffer.length); if (nRead == -1) { break; } out.write(buffer, 0, nRead); } } } private static void copyUsingFileChannels(final File sourceFile, final File destFile) throws IOException { final File parent = destFile.getParentFile(); if (parent != null && !parent.isDirectory() && !(parent.mkdirs() || parent.isDirectory())) { throw new IOException("failed to create the parent directory" + " for " + destFile); } try (FileChannel srcChannel = FileChannel.open(sourceFile.toPath(), StandardOpenOption.READ); FileChannel destChannel = FileChannel.open(destFile.toPath(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { long position = 0; final long count = srcChannel.size(); while (position < count) { final long chunk = Math.min(MAX_IO_CHUNK_SIZE, count - position); position += destChannel.transferFrom(srcChannel, position, chunk); } } } private static void copyUsingStreams(final Resource source, final Resource dest, final boolean append, final Project project) throws IOException { try (InputStream in = source.getInputStream(); OutputStream out = getOutputStream(dest, append, project)) { final byte[] buffer = new byte[FileUtils.BUF_SIZE]; int count = 0; do { out.write(buffer, 0, count); count = in.read(buffer, 0, buffer.length); } while (count != -1); } } private static OutputStream getOutputStream(final Resource resource, final boolean append, final Project project) throws IOException { if (append) { final Appendable a = resource.as(Appendable.class); if (a != null) { return a.getAppendOutputStream(); } String msg = "Appendable OutputStream not available for non-appendable resource " + resource + "; using plain OutputStream"; if (project != null) { project.log(msg, Project.MSG_VERBOSE); } else { System.out.println(msg); } } return resource.getOutputStream(); } public interface ResourceSelectorProvider { ResourceSelector getTargetSelectorForSource(Resource source); } /** * @since Ant 1.9.4 */ public static class ReadOnlyTargetFileException extends IOException { private static final long serialVersionUID = 1L; public ReadOnlyTargetFileException(final File destFile) { super("can't write to read-only destination file " + destFile); } } }