Java tutorial
/* * 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.sis.internal.maven; import java.io.File; import java.io.IOException; import java.io.FilenameFilter; import java.io.FileInputStream; import java.io.InputStream; import java.util.Map; import java.util.Set; import java.util.Enumeration; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.zip.ZipFile; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import org.apache.maven.artifact.Artifact; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.project.MavenProject; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; import static org.apache.sis.internal.maven.Filenames.*; /** * Creates a ZIP file containing the Apache SIS binary distribution. * The created file contains: * * <ul> * <li>the content of the <code>application/sis-console/src/main/artifact</code> directory;</li> * <li>the JAR files of all modules and their dependencies, without their native resources;</li> * <li>the native resources in a separated {@code lib/} directory.</li> * </ul> * * This MOJO can be invoked from the command line in the {@code sis-console} module as below: * * <blockquote><code>mvn package org.apache.sis.core:sis-build-helper:dist</code></blockquote> * * <p><b>Current limitation:</b> * The current implementation uses some hard-coded paths and filenames. * See the <cite>Distribution file</cite> section in * <a href="http://sis.apache.org/build.html">Build from source</a> page * for more information.</p> * * @author Martin Desruisseaux (Geomatys) * @version 1.0 * @since 0.4 * @module */ @Mojo(name = "dist", defaultPhase = LifecyclePhase.INSTALL, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME) public final class Assembler extends AbstractMojo implements FilenameFilter { /** * Project information (name, version, URL). */ @Parameter(property = "project", required = true, readonly = true) private MavenProject project; /** * Base directory of the module to compile. * Artifact content is expected in the {@code "src/main/artifact"} subdirectory. */ @Parameter(property = "basedir", required = true, readonly = true) private String baseDirectory; /** * The root directory (without the "<code>target/binaries</code>" sub-directory) where JARs * are to be copied. It should be the directory of the root <code>pom.xml</code>. */ @Parameter(property = "session.executionRootDirectory", required = true) private String rootDirectory; /** * Invoked by reflection for creating the MOJO. */ public Assembler() { } /** * Creates the distribution file. * * @throws MojoExecutionException if the plugin execution failed. */ @Override public void execute() throws MojoExecutionException { final File sourceDirectory = new File(baseDirectory, ARTIFACT_PATH); if (!sourceDirectory.isDirectory()) { throw new MojoExecutionException("Directory not found: " + sourceDirectory); } final String artifactBase = FINALNAME_PREFIX + project.getVersion(); final File targetFile = distributionFile(rootDirectory, artifactBase + ".zip"); try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(targetFile)) { zip.setLevel(9); appendRecursively(sourceDirectory, artifactBase, zip); /* * At this point, all the "application/sis-console/src/main/artifact" and sub-directories * have been zipped. Now append the JAR files for each module and their dependencies. */ final Map<String, byte[]> nativeFiles = new LinkedHashMap<>(); for (final File file : files(project)) { ZipArchiveEntry entry = new ZipArchiveEntry( artifactBase + '/' + LIB_DIRECTORY + '/' + file.getName()); zip.putArchiveEntry(entry); appendJAR(file, zip, nativeFiles); zip.closeArchiveEntry(); } /* * At this point we finished creating all entries in the ZIP file, except native resources. * Copy them now. */ for (final Map.Entry<String, byte[]> nf : nativeFiles.entrySet()) { ZipArchiveEntry entry = new ZipArchiveEntry(artifactBase + '/' + LIB_DIRECTORY + '/' + nf.getKey()); entry.setUnixMode(0555); // Readable and executable for all, but not writable. zip.putArchiveEntry(entry); zip.write(nf.getValue()); zip.closeArchiveEntry(); nf.setValue(null); } } catch (IOException e) { throw new MojoExecutionException(e.getLocalizedMessage(), e); } } /** * Returns all files to include for the given Maven project. */ private static Set<File> files(final MavenProject project) throws MojoExecutionException { final Set<File> files = new LinkedHashSet<>(); files.add(project.getArtifact().getFile()); for (final Artifact dep : project.getArtifacts()) { final String scope = dep.getScope(); if (Artifact.SCOPE_COMPILE.equalsIgnoreCase(scope) || Artifact.SCOPE_RUNTIME.equalsIgnoreCase(scope)) { files.add(dep.getFile()); } } if (files.remove(null)) { throw new MojoExecutionException( "Invocation of this MOJO shall be done together with a \"package\" Maven phase."); } return files; } /** * Adds the given file in the ZIP file. If the given file is a directory, then this method * recursively adds all files contained in this directory. This method is invoked for zipping * the "application/sis-console/src/main/artifact" directory and sub-directories before to zip. */ private void appendRecursively(final File file, String relativeFile, final ZipArchiveOutputStream out) throws IOException { if (file.isDirectory()) { relativeFile += '/'; } final ZipArchiveEntry entry = new ZipArchiveEntry(file, relativeFile); if (file.canExecute()) { entry.setUnixMode(0744); } out.putArchiveEntry(entry); if (!entry.isDirectory()) { try (FileInputStream in = new FileInputStream(file)) { in.transferTo(out); } } out.closeArchiveEntry(); if (entry.isDirectory()) { for (final String filename : file.list(this)) { appendRecursively(new File(file, filename), relativeFile.concat(filename), out); } } } /** * The filter to use for selecting the files to be included in the ZIP file. * * @param directory the directory. * @param filename the filename. * @return {@code true} if the given file should be included in the ZIP file. */ @Override public boolean accept(final File directory, final String filename) { return !filename.isEmpty() && filename.charAt(0) != '.' && !filename.equals(CONTENT_FILE); } /** * Returns {@code true} if the given JAR file contains at least one resource in the * {@value Filenames#NATIVE_DIRECTORY} directory. Those files will need to be rewritten * in order to exclude those resources. */ private static boolean hasNativeResources(final ZipFile in) { final Enumeration<? extends ZipEntry> entries = in.entries(); while (entries.hasMoreElements()) { if (entries.nextElement().getName().startsWith(NATIVE_DIRECTORY)) { return true; } } return false; } /** * Copies a JAR file in the given ZIP file, except the native resources which are stored in the given map. * * @param file the JAR file to copy. * @param bundle destination where to copy the JAR file. * @param nativeFiles where to store the native resources. */ private void appendJAR(final File file, final ZipArchiveOutputStream bundle, final Map<String, byte[]> nativeFiles) throws IOException { try (ZipFile in = new ZipFile(file)) { if (hasNativeResources(in)) { final ZipOutputStream out = new ZipOutputStream(bundle); out.setLevel(9); final Enumeration<? extends ZipEntry> entries = in.entries(); while (entries.hasMoreElements()) { final ZipEntry entry = entries.nextElement(); final String name = entry.getName(); try (InputStream eis = in.getInputStream(entry)) { if (!name.startsWith(NATIVE_DIRECTORY)) { out.putNextEntry(new ZipEntry(name)); eis.transferTo(out); // Copy the entry verbatim. out.closeEntry(); } else if (!entry.isDirectory()) { final long size = entry.getSize(); // For memorizing the entry without copying it now. if (size <= 0 || size > Integer.MAX_VALUE) { throw new IOException(String.format( "Errors while copying %s:%n" + "Unsupported size for \"%s\" entry: %d", file, name, size)); } final byte[] content = new byte[(int) size]; final int actual = eis.read(content); if (actual != size) { throw new IOException(String.format( "Errors while copying %s:%n" + "Expected %d bytes in \"%s\" but found %d", file, size, name, actual)); } if (nativeFiles.put(name.substring(NATIVE_DIRECTORY.length()), content) != null) { throw new IOException(String .format("Errors while copying %s:%n" + "Duplicated entry: %s", file, name)); } } } } out.finish(); return; } } /* * If the JAR file has no native resources to exclude, we can copy the JAR file verbatim. * This is faster than inflating and deflating again the JAR file. */ try (FileInputStream in = new FileInputStream(file)) { in.transferTo(bundle); } } }