RepeatableArchive.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.tomcat.buildutil;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.FileTime;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileSet;

/**
 * Ant task to assist with repeatable builds.
 * <p>
 * While originally written to address an issue with Javadoc output, this task
 * takes a generic approach that could be used with any archive. The task takes
 * a set of zip (or jar, war etc) files as its input and sets the last modified
 * time of every file in the archive to be the same as the last modified time
 * of the archive.
 */
public class RepeatableArchive extends Task {

    private final List<FileSet> filesets = new ArrayList<>();

    private String datetime;
    private String pattern;

    /**
     * Sets the files to be processed
     *
     * @param fs The fileset to be processed.
     */
    public void addFileset(FileSet fs) {
        filesets.add(fs);
    }


    public void setDatetime(String datetime) {
        this.datetime = datetime;
    }


    public void setPattern(String pattern) {
        this.pattern = pattern;
    }


    @Override
    public void execute() throws BuildException {

        SimpleDateFormat sdf = new SimpleDateFormat(pattern);
        Date date;
        try {
            date = sdf.parse(datetime);
        } catch (ParseException e) {
            throw new BuildException(e);
        }

        byte[] buf = new byte[8192];
        FileTime lastModified = FileTime.fromMillis(date.getTime());

        for (FileSet fs : filesets) {
            DirectoryScanner ds = fs.getDirectoryScanner(getProject());
            File basedir = ds.getBasedir();
            String[] files = ds.getIncludedFiles();
            for (String file : files) {
                File archive = new File(basedir, file);
                File oldArchive = new File(basedir, file + ".old");

                try {
                    Files.move(archive.toPath(), oldArchive.toPath(), StandardCopyOption.ATOMIC_MOVE);

                    try (ZipFile oldZipFile = new ZipFile(oldArchive);
                            ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(archive))) {

                        Enumeration<? extends ZipEntry> oldEntries = oldZipFile.entries();
                        while (oldEntries.hasMoreElements()) {
                            ZipEntry oldEntry = oldEntries.nextElement();

                            ZipEntry entry = new ZipEntry(oldEntry.getName());
                            entry.setLastModifiedTime(lastModified);

                            zipOut.putNextEntry(entry);

                            InputStream is = oldZipFile.getInputStream(oldEntry);

                            int numRead;
                            while ((numRead = is.read(buf)) >= 0) {
                                zipOut.write(buf, 0, numRead);
                            }
                        }
                    }

                    if (!archive.setLastModified(lastModified.toMillis())) {
                        throw new BuildException("setLastModified failed for [" + archive.getAbsolutePath() + "]");
                    }
                    Files.delete(oldArchive.toPath());
                } catch (IOException ioe) {
                    throw new BuildException(ioe);
                }
            }
        }
    }
}