ResourceUtils.java

/*
 *  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, project);
                    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 {

        if (areSame(source, dest)) {
            // copying the "same" file to itself will corrupt the file, so we skip it
            log(project, "Skipping (self) copy of " + source +  " to " + dest);
            return;
        }

        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 {

        if (areSame(source, dest)) {
            // copying the "same" file to itself will corrupt the file, so we skip it
            log(project, "Skipping (self) copy of " + source +  " to " + dest);
            return;
        }

        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, final Project project)
        throws IOException {

        if (FileUtils.getFileUtils().areSame(sourceFile, destFile)) {
            // copying the "same" file to itself will corrupt the file, so we skip it
            log(project, "Skipping (self) copy of " + sourceFile +  " to " + destFile);
            return;
        }
        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 {

        if (areSame(source, dest)) {
            // copying the "same" file to itself will corrupt the file, so we skip it
            log(project, "Skipping (self) copy of " + source +  " to " + dest);
            return;
        }
        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();
    }

    private static boolean areSame(final Resource resource1, final Resource resource2) throws IOException {
        if (resource1 == null || resource2 == null) {
            return false;
        }
        final FileProvider fileResource1 = resource1.as(FileProvider.class);
        if (fileResource1 == null) {
            return false;
        }
        final FileProvider fileResource2 = resource2.as(FileProvider.class);
        if (fileResource2 == null) {
            return false;
        }
        return FileUtils.getFileUtils().areSame(fileResource1.getFile(), fileResource2.getFile());
    }

    private static void log(final Project project, final String message) {
        log(project, message, Project.MSG_VERBOSE);
    }

    private static void log(final Project project, final String message, final int level) {
        if (project == null) {
            System.out.println(message);
        } else {
            project.log(message, level);
        }
    }

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