Java tutorial
package org.codehaus.mojo.webstart; /* * 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. */ import org.apache.commons.collections.MapUtils; import org.apache.commons.lang.StringUtils; import org.apache.maven.artifact.repository.ArtifactRepository; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; import org.codehaus.mojo.webstart.dependency.filenaming.DependencyFilenameStrategy; import org.codehaus.mojo.webstart.pack200.Pack200Config; import org.codehaus.mojo.webstart.pack200.Pack200Tool; import org.codehaus.mojo.webstart.sign.SignConfig; import org.codehaus.mojo.webstart.sign.SignTool; import org.codehaus.mojo.webstart.util.ArtifactUtil; import org.codehaus.mojo.webstart.util.IOUtil; import org.codehaus.mojo.webstart.util.JarUtil; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * The superclass for all JNLP generating MOJOs. * * @author Kevin Stembridge * @author $LastChangedBy$ * @version $Revision$ * @since 28 May 2007 */ public abstract class AbstractBaseJnlpMojo extends AbstractMojo { // ---------------------------------------------------------------------- // Constants // ---------------------------------------------------------------------- private static final String DEFAULT_RESOURCES_DIR = "src/main/jnlp/resources"; /** * unprocessed files (that will be signed) are prefixed with this */ private static final String UNPROCESSED_PREFIX = "unprocessed_"; /** * Suffix extension of a jar file. */ public static final String JAR_SUFFIX = ".jar"; // ---------------------------------------------------------------------- // Mojo Parameters // ---------------------------------------------------------------------- /** * Local repository. */ @Parameter(defaultValue = "${localRepository}", required = true, readonly = true) private ArtifactRepository localRepository; /** * The collection of remote artifact repositories. */ @Parameter(defaultValue = "${project.remoteArtifactRepositories}", required = true, readonly = true) private List<ArtifactRepository> remoteRepositories; /** * The directory in which files will be stored prior to processing. */ @Parameter(property = "jnlp.workDirectory", defaultValue = "${project.build.directory}/jnlp", required = true) private File workDirectory; /** * The path where the libraries are placed within the jnlp structure. */ @Parameter(property = "jnlp.libPath", defaultValue = "") protected String libPath; /** * The location of the directory (relative or absolute) containing non-jar resources that * are to be included in the JNLP bundle. */ @Parameter(property = "jnlp.resourcesDirectory") private File resourcesDirectory; /** * The location where the JNLP Velocity template files are stored. */ @Parameter(property = "jnlp.templateDirectory", defaultValue = "${project.basedir}/src/main/jnlp", required = true) private File templateDirectory; /** * The Pack200 Config. * * @since 1.0-beta-4 */ @Parameter private Pack200Config pack200; /** * The Sign Config. */ @Parameter private SignConfig sign; /** * Indicates whether or not gzip archives will be created for each of the jar * files included in the webstart bundle. */ @Parameter(property = "jnlp.gzip", defaultValue = "false") private boolean gzip; /** * Enable verbose output. */ @Parameter(property = "webstart.verbose", alias = "verbose", defaultValue = "false") private boolean verbose; /** * Set to true to exclude all transitive dependencies. * * @parameter */ @Parameter(property = "jnlp.excludeTransitive") private boolean excludeTransitive; /** * The code base to use on the generated jnlp files. * * @since 1.0-beta-2 */ @Parameter(property = "jnlp.codebase", defaultValue = "${project.url}/jnlp") private String codebase; /** * Encoding used to read and write jnlp files. * <p/> * <strong>Note:</strong> If this property is not defined, then will use a default value {@code utf-8}. * * @since 1.0-beta-2 */ @Parameter(property = "jnlp.encoding", defaultValue = "${project.build.sourceEncoding}") private String encoding; /** * Define whether to remove existing signatures. */ @Parameter(property = "jnlp.unsign", alias = "unsign", defaultValue = "false") private boolean unsignAlreadySignedJars; /** * To authorize or not to unsign some already signed jar. * <p/> * If set to false and the {@code unsign} parameter is set to {@code true} then the build will fail if there is * a jar to unsign, to avoid this use then the extension jnlp component. * * @since 1.0-beta-2 */ @Parameter(property = "jnlp.canUnsign", defaultValue = "true") private boolean canUnsign; /** * To update manifest entries of all jar resources. * <p/> * Since jdk 1.7u45, you need to add some entries to be able to open jnlp files in High security level. * See http://www.oracle.com/technetwork/java/javase/7u45-relnotes-2016950.html * <p/> * <strong>Note:</strong> Won't affect any already signed jar resources if you configuration does not authorize it. * <p/> * Si parameters {@link #unsignAlreadySignedJars} and {@link #canUnsign}. * * @since 1.0-beta-4 */ @Parameter private Map<String, String> updateManifestEntries; /** * Compile class-path elements used to search for the keystore * (if kestore location was prefixed by {@code classpath:}). * * @since 1.0-beta-4 */ @Parameter(defaultValue = "${project.compileClasspathElements}", required = true, readonly = true) private List<?> compileClassPath; /** * Naming strategy for dependencies of a jnlp application. * * The strategy purpose is to transform the name of the dependency file. * * The actual authorized values are: * <ul> * <li><strong>simple</strong>: artifactId[-classifier]-version.jar</li> * <li><strong>full</strong>: groupId-artifactId[-classifier]-version.jar</li> * </ul> * * Default value is {@code full} which avoid any colision of naming. * * @since 1.0-beta-5 */ @Parameter(property = "jnlp.filenameMapping", defaultValue = "simple", required = true) private String filenameMapping; /** * Use unique version for any snapshot dependency, or just use the {@code -SNAPSHOT} version suffix. * * @since 1.0-beta-7 */ @Parameter(property = "jnlp.useUniqueVersions", defaultValue = "false") private boolean useUniqueVersions; // ---------------------------------------------------------------------- // Components // ---------------------------------------------------------------------- /** * Sign tool. */ @Component private SignTool signTool; /** * All available pack200 tools. * <p/> * We use a plexus list injection instead of a direct component injection since for a jre 1.4, we will at the * moment have no implementation of this tool. * <p/> * Later in the execute of mojo, we will check if at least one implementation is available if required. * * @since 1.0-beta-2 */ @Component(role = Pack200Tool.class) private Pack200Tool pack200Tool; /** * Artifact helper. * * @since 1.0-beta-4 */ @Component private ArtifactUtil artifactUtil; /** * io helper. * * @since 1.0-beta-4 */ @Component private IOUtil ioUtil; /** * Jar util. * * @since 1.0-beta-4 */ @Component(hint = "default") private JarUtil jarUtil; /** * All dependency filename strategy indexed by theire role-hint. * * @since 1.0-beta-5 */ @Component(role = DependencyFilenameStrategy.class) private Map<String, DependencyFilenameStrategy> dependencyFilenameStrategyMap; // ---------------------------------------------------------------------- // Fields // ---------------------------------------------------------------------- /** * List of detected modified artifacts (will then re-apply stuff on them). */ private final List<String> modifiedJnlpArtifacts = new ArrayList<String>(); // the jars to sign and pack are selected if they are prefixed by UNPROCESSED_PREFIX. // as the plugin copies the new versions locally before signing/packing them // we just need to see if the plugin copied a new version // We achieve that by only filtering files modified after the plugin was started // Note: if other files (the pom, the keystore config) have changed, one needs to clean private final FileFilter unprocessedJarFileFilter; /** * Filter of processed jar files. */ private final FileFilter processedJarFileFilter; /** * Filter of jar files that need to be pack200. */ private final FileFilter unprocessedPack200FileFilter; /** * The dependency filename strategy. */ private DependencyFilenameStrategy dependencyFilenameStrategy; /** * Creates a new {@code AbstractBaseJnlpMojo}. */ public AbstractBaseJnlpMojo() { processedJarFileFilter = new FileFilter() { /** * {@inheritDoc} */ public boolean accept(File pathname) { return pathname.isFile() && pathname.getName().endsWith(JAR_SUFFIX) && !pathname.getName().startsWith(UNPROCESSED_PREFIX); } }; unprocessedJarFileFilter = new FileFilter() { /** * {@inheritDoc} */ public boolean accept(File pathname) { return pathname.isFile() && pathname.getName().startsWith(UNPROCESSED_PREFIX) && pathname.getName().endsWith(JAR_SUFFIX); } }; unprocessedPack200FileFilter = new FileFilter() { /** * {@inheritDoc} */ public boolean accept(File pathname) { return pathname.isFile() && pathname.getName().startsWith(UNPROCESSED_PREFIX) && (pathname.getName().endsWith(JAR_SUFFIX + Pack200Tool.PACK_GZ_EXTENSION) || pathname.getName().endsWith(JAR_SUFFIX + Pack200Tool.PACK_EXTENSION)); } }; } // ---------------------------------------------------------------------- // Public Methods // ---------------------------------------------------------------------- public abstract MavenProject getProject(); /** * Returns the library path. This is ths subpath within the working directory, where the libraries are placed. * If the path is not configured it is <code>null</code>. * * @return the library path or <code>null</code> if not configured. */ public String getLibPath() { if (StringUtils.isBlank(libPath)) { return null; } return libPath; } /** * Returns the flag that indicates whether or not jar resources * will be compressed using pack200. * * @return Returns the value of the pack200.enabled field. */ public boolean isPack200() { return pack200 != null && pack200.isEnabled(); } /** * Returns the files to be passed without pack200 compression. * * @return Returns the list value of the pack200.passFiles. */ public List<String> getPack200PassFiles() { return pack200 == null ? null : pack200.getPassFiles(); } // ---------------------------------------------------------------------- // Protected Methods // ---------------------------------------------------------------------- /** * Returns the working directory. This is the directory in which files and resources * will be placed in order to be processed prior to packaging. * * @return Returns the value of the workDirectory field. */ protected File getWorkDirectory() { return workDirectory; } /** * Returns the library directory. If not libPath is configured, the working directory is returned. * * @return Returns the value of the libraryDirectory field. */ protected File getLibDirectory() { if (getLibPath() != null) { return new File(getWorkDirectory(), getLibPath()); } return getWorkDirectory(); } /** * Returns the location of the directory containing * non-jar resources that are to be included in the JNLP bundle. * * @return Returns the value of the resourcesDirectory field, never null. */ protected File getResourcesDirectory() { if (resourcesDirectory == null) { resourcesDirectory = new File(getProject().getBasedir(), DEFAULT_RESOURCES_DIR); } return resourcesDirectory; } /** * Returns the file handle to the directory containing the Velocity templates for the JNLP * files to be generated. * * @return Returns the value of the templateDirectory field. */ protected File getTemplateDirectory() { return templateDirectory; } /** * Returns the local artifact repository. * * @return Returns the value of the localRepository field. */ protected ArtifactRepository getLocalRepository() { return localRepository; } /** * Returns the collection of remote artifact repositories for the current * Maven project. * * @return Returns the value of the remoteRepositories field. */ protected List<ArtifactRepository> getRemoteRepositories() { return remoteRepositories; } /** * Returns jar signing configuration element. * * @return Returns the value of the sign field. */ protected SignConfig getSign() { return sign; } /** * Returns the code base to inject in the generated jnlp. * * @return Returns the value of codebase field. */ protected String getCodebase() { return codebase; } /** * Returns the flag that indicates whether or not a gzip should be * created for each jar resource. * * @return Returns the value of the gzip field. */ protected boolean isGzip() { return gzip; } /** * Returns the flag that indicates whether or not to provide verbose output. * * @return Returns the value of the verbose field. */ protected boolean isVerbose() { return verbose; } /** * Returns the flag that indicates whether or not all transitive dependencies will be excluded * from the generated JNLP bundle. * * @return Returns the value of the excludeTransitive field. */ protected boolean isExcludeTransitive() { return this.excludeTransitive; } /** * Returns the collection of artifacts that have been modified * since the last time this mojo was run. * * @return Returns the value of the modifiedJnlpArtifacts field. */ protected List<String> getModifiedJnlpArtifacts() { return modifiedJnlpArtifacts; } /** * @return the mojo encoding to use to write files. */ protected String getEncoding() { if (StringUtils.isEmpty(encoding)) { encoding = "utf-8"; getLog().warn("No encoding defined, will use the default one : " + encoding); } return encoding; } protected DependencyFilenameStrategy getDependencyFilenameStrategy() { if (dependencyFilenameStrategy == null) { dependencyFilenameStrategy = dependencyFilenameStrategyMap.get(filenameMapping); } return dependencyFilenameStrategy; } protected boolean isUseUniqueVersions() { return useUniqueVersions; } protected void checkDependencyFilenameStrategy() throws MojoExecutionException { if (getDependencyFilenameStrategy() == null) { dependencyFilenameStrategy = dependencyFilenameStrategyMap.get(filenameMapping); if (dependencyFilenameStrategy == null) { throw new MojoExecutionException("Could not find filenameMapping named '" + filenameMapping + "', use one of the following one: " + dependencyFilenameStrategyMap.keySet()); } } } /** * Conditionally copy the jar file into the target directory. * The operation is not performed when a signed target file exists and is up to date. * The signed target file name is taken from the <code>sourceFile</code> name.E * The unsigned target file name is taken from the <code>sourceFile</code> name prefixed with UNPROCESSED_PREFIX. * TODO this is confusing if the sourceFile is already signed. By unsigned we really mean 'unsignedbyus' * * @param sourceFile source file to copy * @param targetDirectory location of the target directory where to copy file * @param targetFilename [optional] to change the target filename to use (if {@code null} will * use the sourceFile name). * @return <code>true</code> when the file was copied, <code>false</code> otherwise. * @throws IllegalArgumentException if sourceFile is <code>null</code> or * <code>sourceFile.getName()</code> is <code>null</code> * @throws MojoExecutionException if an error occurs attempting to copy the file. */ protected boolean copyJarAsUnprocessedToDirectoryIfNecessary(File sourceFile, File targetDirectory, String targetFilename) throws MojoExecutionException { if (sourceFile == null) { throw new IllegalArgumentException("sourceFile is null"); } if (targetFilename == null) { targetFilename = sourceFile.getName(); } File signedTargetFile = new File(targetDirectory, targetFilename); File unsignedTargetFile = toUnprocessFile(targetDirectory, targetFilename); boolean shouldCopy = !signedTargetFile.exists() || (signedTargetFile.lastModified() < sourceFile.lastModified()); shouldCopy &= (!unsignedTargetFile.exists() || (unsignedTargetFile.lastModified() < sourceFile.lastModified())); if (shouldCopy) { getIoUtil().copyFile(sourceFile, unsignedTargetFile); } else { getLog().debug("Source file hasn't changed. Do not reprocess " + signedTargetFile + " with " + sourceFile + "."); } return shouldCopy; } /** * If sign is enabled, sign the jars, otherwise rename them into final jars * * @throws MojoExecutionException if can not sign or rename jars */ protected void signOrRenameJars() throws MojoExecutionException { if (sign != null) { try { ClassLoader loader = getCompileClassLoader(); sign.init(getWorkDirectory(), getLog().isDebugEnabled(), signTool, loader); } catch (MalformedURLException e) { throw new MojoExecutionException("Could not create classloader", e); } if (unsignAlreadySignedJars) { removeExistingSignatures(getLibDirectory()); } if (isPack200()) { //TODO tchemit use a temporary directory to pack-unpack // http://java.sun.com/j2se/1.5.0/docs/guide/deployment/deployment-guide/pack200.html // we need to pack then unpack the files before signing them unpackJars(getLibDirectory()); // As out current Pack200 ant tasks don't give us the ability to use a temporary area for // creating those temporary packing, we have to delete the temporary files. ioUtil.deleteFiles(getLibDirectory(), unprocessedPack200FileFilter); // specs says that one should do it twice when there are unsigned jars?? // Pack200.unpackJars( applicationDirectory, updatedPack200FileFilter ); } if (MapUtils.isNotEmpty(updateManifestEntries)) { updateManifestEntries(getLibDirectory()); } int signedJars = signJars(getLibDirectory()); if (signedJars != getModifiedJnlpArtifacts().size()) { throw new IllegalStateException( "The number of signed artifacts (" + signedJars + ") differ from the number of modified " + "artifacts (" + getModifiedJnlpArtifacts().size() + "). Implementation error"); } } else { makeUnprocessedFilesFinal(getLibDirectory()); } if (isPack200()) { verboseLog("-- Pack jars"); pack200Jars(getLibDirectory(), processedJarFileFilter); } } protected void pack200Jars(File directory, FileFilter filter) throws MojoExecutionException { try { getPack200Tool().packJars(directory, filter, isGzip(), getPack200PassFiles()); } catch (IOException e) { throw new MojoExecutionException("Could not pack200 jars: ", e); } } protected URL findDefaultTemplateURL(JnlpFileType fileType) { return getClass().getClassLoader().getResource(fileType.getDefaultTemplateName()); } /** * @return something of the form jar:file:..../webstart-maven-plugin-.....jar!/ */ protected String getWebstartJarURLForVelocity() { String url = findDefaultTemplateURL(JnlpFileType.application).toString(); return url.substring(0, url.indexOf("!") + 2); } protected boolean isJarSigned(File jarFile) throws MojoExecutionException { return signTool.isJarSigned(jarFile); } protected ArtifactUtil getArtifactUtil() { return artifactUtil; } protected IOUtil getIoUtil() { return ioUtil; } protected Pack200Tool getPack200Tool() { return pack200Tool; } /** * Log as info when verbose or info is enabled, as debug otherwise. * * @param msg the message to display */ protected void verboseLog(String msg) { if (isVerbose()) { getLog().info(msg); } else { getLog().debug(msg); } } // ---------------------------------------------------------------------- // Private Methods // ---------------------------------------------------------------------- private void unpackJars(File directory) throws MojoExecutionException { getLog().info("-- Unpack jars before sign operation "); verboseLog( "see http://docs.oracle.com/javase/7/docs/technotes/guides/deployment/deployment-guide/pack200.html"); // pack pack200Jars(directory, unprocessedJarFileFilter); // then unpack try { getPack200Tool().unpackJars(directory, unprocessedPack200FileFilter); } catch (IOException e) { throw new MojoExecutionException("Could not unpack200 jars: ", e); } } private int makeUnprocessedFilesFinal(File directory) throws MojoExecutionException { File[] jarFiles = directory.listFiles(unprocessedJarFileFilter); getLog().debug( "makeUnprocessedFilesFinal in " + directory + " found " + jarFiles.length + " file(s) to rename"); if (jarFiles.length == 0) { return 0; } for (File unprocessedJarFile : jarFiles) { File finalJar = toProcessFile(unprocessedJarFile); ioUtil.deleteFile(finalJar); ioUtil.renameTo(unprocessedJarFile, finalJar); } return jarFiles.length; } /** * @param directory location of directory where to update manifest entries jars * @throws MojoExecutionException if can not update manifest entries jars */ private void updateManifestEntries(File directory) throws MojoExecutionException { File[] jarFiles = directory.listFiles(unprocessedJarFileFilter); getLog().info("-- Update manifest entries"); getLog().debug("updateManifestEntries in " + directory + " found " + jarFiles.length + " jar(s) to treat"); if (jarFiles.length == 0) { return; } for (File unprocessedJarFile : jarFiles) { verboseLog("Update manifest " + toProcessFile(unprocessedJarFile).getName()); jarUtil.updateManifestEntries(unprocessedJarFile, updateManifestEntries); } } /** * @param directory location of directory where to sign jars * @return the number of signed jars * @throws MojoExecutionException if can not sign jars */ private int signJars(File directory) throws MojoExecutionException { File[] jarFiles = directory.listFiles(unprocessedJarFileFilter); getLog().info("-- Sign jars"); getLog().debug("signJars in " + directory + " found " + jarFiles.length + " jar(s) to sign"); if (jarFiles.length == 0) { return 0; } boolean signVerify = sign.isVerify(); for (File unprocessedJarFile : jarFiles) { File signedJar = toProcessFile(unprocessedJarFile); ioUtil.deleteFile(signedJar); verboseLog("Sign " + signedJar.getName()); signTool.sign(sign, unprocessedJarFile, signedJar); getLog().debug("lastModified signedJar:" + signedJar.lastModified() + " unprocessed signed Jar:" + unprocessedJarFile.lastModified()); if (signVerify) { verboseLog("Verify signature of " + signedJar.getName()); signTool.verify(sign, signedJar, isVerbose()); } // remove unprocessed files // TODO wouldn't have to do that if we copied the unprocessed jar files in a temporary area ioUtil.deleteFile(unprocessedJarFile); } return jarFiles.length; } /** * Removes the signature of the files in the specified directory which satisfy the * specified filter. * * @param workDirectory working directory used to unsign jars * @return the number of unsigned jars * @throws MojoExecutionException if could not remove signatures */ private int removeExistingSignatures(File workDirectory) throws MojoExecutionException { getLog().info("-- Remove existing signatures"); // cleanup tempDir if exists File tempDir = new File(workDirectory, "temp_extracted_jars"); ioUtil.removeDirectory(tempDir); // recreate temp dir ioUtil.makeDirectoryIfNecessary(tempDir); // process jars File[] jarFiles = workDirectory.listFiles(unprocessedJarFileFilter); for (File jarFile : jarFiles) { if (isJarSigned(jarFile)) { if (!canUnsign) { throw new MojoExecutionException("neverUnsignAlreadySignedJar is set to true and a jar file [" + jarFile + " was asked to be unsign,\n please prefer use in this case an extension for " + "signed jars or not set to true the neverUnsignAlreadySignedJar parameter, Make " + "your choice:)"); } verboseLog("Remove signature " + toProcessFile(jarFile).getName()); signTool.unsign(jarFile, isVerbose()); } else { verboseLog("Skip not signed " + toProcessFile(jarFile).getName()); } } // cleanup tempDir ioUtil.removeDirectory(tempDir); return jarFiles.length; // FIXME this is wrong. Not all jars are signed. } private ClassLoader getCompileClassLoader() throws MalformedURLException { URL[] urls = new URL[compileClassPath.size()]; for (int i = 0; i < urls.length; i++) { String spec = compileClassPath.get(i).toString(); URL url = new File(spec).toURI().toURL(); urls[i] = url; } return new URLClassLoader(urls); } private File toUnprocessFile(File targetDirectory, String sourceName) { if (sourceName.startsWith(UNPROCESSED_PREFIX)) { throw new IllegalStateException(sourceName + " does start with " + UNPROCESSED_PREFIX); } String targetFilename = UNPROCESSED_PREFIX + sourceName; return new File(targetDirectory, targetFilename); } private File toProcessFile(File source) { if (!source.getName().startsWith(UNPROCESSED_PREFIX)) { throw new IllegalStateException(source.getName() + " does not start with " + UNPROCESSED_PREFIX); } String targetFilename = source.getName().substring(UNPROCESSED_PREFIX.length()); return new File(source.getParentFile(), targetFilename); } }