Java tutorial
/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- * * Copyright (c) 2013-2014 Edugility LLC. * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, copy, * modify, merge, publish, distribute, sublicense and/or sell copies * of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * * The original copy of this license is available at * http://www.opensource.org/license/mit-license.html. */ package com.edugility.liquibase.maven; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Writer; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import com.edugility.maven.Artifacts; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.resolver.ArtifactResolver; import org.apache.maven.artifact.resolver.ArtifactResolutionException; import org.apache.maven.artifact.resolver.ArtifactResolutionRequest; // for javadoc only import org.apache.maven.artifact.resolver.filter.ArtifactFilter; import org.apache.maven.artifact.repository.ArtifactRepository; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugin.logging.Log; import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.model.Build; import org.apache.maven.project.MavenProject; import org.apache.maven.shared.dependency.graph.DependencyGraphBuilder; import org.apache.maven.shared.dependency.graph.DependencyGraphBuilderException; import org.mvel2.integration.impl.MapVariableResolverFactory; import org.mvel2.templates.CompiledTemplate; import org.mvel2.templates.TemplateCompiler; import org.mvel2.templates.TemplateRuntime; /** * Scans the test classpath in dependency order for <a * href="http://www.liquibase.org/">Liquibase</a> <a * href="http://www.liquibase.org/documentation/databasechangelog.html">changelog</a> * fragments and assembles a master changelog that <a * href="http://www.liquibase.org/documentation/include.html">includes</a> * them all in dependency order. * * @author <a href="http://about.me/lairdnelson" * target="_parent">Laird Nelson</a> * * @see AbstractLiquibaseMojo */ @Mojo(name = "assembleChangeLog", requiresDependencyResolution = ResolutionScope.TEST) public class AssembleChangeLogMojo extends AbstractLiquibaseMojo { /* * Static fields. */ /** * The platform's line separator; "{@code \\n}" by default. This * field is never {@code null}. */ private static final String LS = System.getProperty("line.separator", "\n"); /* * Instance fields and plugin parameters. */ /** * The {@link DependencyGraphBuilder} injected by Maven. This field * is never {@code null} during normal plugin execution. * * @see #getDependencyGraphBuilder() * * @see #setDependencyGraphBuilder(DependencyGraphBuilder) */ @Component private DependencyGraphBuilder dependencyGraphBuilder; /** * The {@link ArtifactResolver} injected by Maven. This field is * never {@code null} during normal plugin execution. * * @see #getArtifactResolver() * * @see #setArtifactResolver(ArtifactResolver) */ @Component private ArtifactResolver artifactResolver; /** * Whether or not this plugin execution should be skipped; {@code * false} by default. * * @see #getSkip() * * @see #setSkip(boolean) */ @Parameter(defaultValue = "false") private boolean skip; /** * The local {@link ArtifactRepository} injected by Maven. This * field is never {@code null} during normal plugin execution. * * @see #getLocalRepository() * * @see #setLocalRepository(ArtifactRepository) */ @Parameter(defaultValue = "${localRepository}", readonly = true, required = true) private ArtifactRepository localRepository; /** * An {@link ArtifactFilter} to use to limit what dependencies are * scanned for changelog fragments; {@code null} by default. * * @see #getArtifactFilter() * * @see #setArtifactFilter(ArtifactFilter) * * @see <a * href="http://maven.apache.org/guides/mini/guide-configuring-plugins.html#Mapping_Complex_Objects">Guide * to Configuring Plug-Ins</a> */ @Parameter private ArtifactFilter artifactFilter; /** * A list of classpath resource names that identity <a * href="http://liquibase.org/">Liquibase</a> changelogs; {@code * META-INF/liquibase/changelog.xml} by default. * * @see #getChangeLogResourceNames() * * @see #setChangeLogResourceNames(List) */ @Parameter(defaultValue = "META-INF/liquibase/changelog.xml", required = true) private List<String> changeLogResourceNames; /** * The classpath resource name of the <a * href="http://mvel.codehaus.org/">MVEL</a> template that will be * used to aggregate all the changelog fragments together; {@code * changelog-template.mvl} by default; typically found within this * plugin's own {@code .jar} file, but users may wish to supply an * alternate template. * * @see #getChangeLogTemplateResourceName() * * @see #setChangeLogTemplateResourceName(String) */ @Parameter private String changeLogTemplateResourceName; /** * The full path to the changelog that will be generated; * <code>${project.build.directory}/generated-sources/liquibase/changelog.xml</code> * by default. * * @see #getOutputFile() * * @see #setOutputFile(File) * * @see <a * href="http://maven.apache.org/guides/mini/guide-configuring-plugins.html#Configuring_Parameters">Guide * to Configuring Plug-Ins</a> */ @Parameter(defaultValue = "${project.build.directory}/generated-sources/liquibase/changelog.xml", required = true) private File outputFile; /** * The version of the proper XSD file to use that defines the * contents of a <a href="http://liquibase.org/">Liquibase</a> * changelog file; {@code 3.0} by default. * * @see #getDatabaseChangeLogXsdVersion() * * @see #setDatabaseChangeLogXsdVersion(String) */ @Parameter(defaultValue = "3.0", required = true) private String databaseChangeLogXsdVersion; /** * A set of {@link Properties} defining <a * href="http://www.liquibase.org/documentation/changelog_parameters.html">changelog * parameters</a>; {@code null} by default. * * @see #getChangeLogParameters() * * @see #setChangeLogParameters(Properties) * * @see <a * href="http://maven.apache.org/guides/mini/guide-configuring-plugins.html#Mapping_Properties">Guide * to Configuring Plug-Ins</a> */ @Parameter private Properties changeLogParameters; /** * The character encoding to use while reading in the changelog <a * href="http://mvel.codehaus.org/">MVEL</a> template; * <code>${project.build.sourceEncoding}</code> by default. * * @see #getTemplateCharacterEncoding() * * @see #setTemplateCharacterEncoding(String) * * @see <a * href="http://docs.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html#iana">Standard * character encoding names provided by Java SE</a> */ @Parameter(required = true, defaultValue = "${project.build.sourceEncoding}") private String templateCharacterEncoding; /** * The character encoding to use while writing the generated * changelog; <code>${project.build.sourceEncoding}</code> by * default. * * @see #getChangeLogCharacterEncoding() * * @see #setChangeLogCharacterEncoding(String) * * @see <a * href="http://docs.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html#iana">Standard * character encoding names provided by Java SE</a> */ @Parameter(required = true, defaultValue = "${project.build.sourceEncoding}") private String changeLogCharacterEncoding; /* * Constructors. */ /** * Creates a new {@link AssembleChangeLogMojo}. */ public AssembleChangeLogMojo() { super(); } /* * Instance methods. */ /* * Simple properties. */ /** * Returns {@code true} if the {@link #execute()} method should take * no action. * * @return {@code true} if the {@link #execute()} method should take * no action; {@code false} otherwise * * @see #setSkip(boolean) */ public boolean getSkip() { return this.skip; } /** * Sets whether the {@link #execute()} method will take any action. * * @param skip if {@code true}, then the {@link #execute()} method * will take no action * * @see #getSkip() */ public void setSkip(final boolean skip) { this.skip = skip; } /** * Returns the {@link ArtifactRepository} that represents the * current local Maven repository. This method may return {@code * null}. * * <p>This method may return {@code null}.</p> * * @return an {@link ArtifactRepository}, or {@code null} * * @see #setLocalRepository(ArtifactRepository) */ public ArtifactRepository getLocalRepository() { return this.localRepository; } /** * Sets the {@link ArtifactRepository} to be used as the current * local Maven repository. * * <p>This method is normally used for testing and mocking purposes * only.</p> * * @param localRepository the {@link ArtifactRepository} * representing the current local Maven repository; must not be * {@code null} * * @exception IllegalArgumentException if {@code localRepository} is * {@code null} * * @see #getLocalRepository() */ public void setLocalRepository(final ArtifactRepository localRepository) { if (localRepository == null) { throw new IllegalArgumentException("localRepository", new NullPointerException("localRepository")); } this.localRepository = localRepository; } /** * Returns an {@link ArtifactFilter} that may be used to filter the * {@link Artifact}s whose {@linkplain Artifact#getFile() associated * <code>File</code>s} are inspected for changelog fragments. * * <p>This method may return {@code null}.</p> * * @return an {@link ArtifactFilter}, or {@code null} * * @see #setArtifactFilter(ArtifactFilter) * * @see <a * href="http://maven.apache.org/guides/mini/guide-configuring-plugins.html#Mapping_Complex_Objects">Guide * to Configuring Plug-Ins</a> */ public ArtifactFilter getArtifactFilter() { return this.artifactFilter; } /** * Installs an {@link ArtifactFilter} that will be used to filter * the {@link Artifact}s whose {@linkplain Artifact#getFile() * associated <code>File</code>s} are inspected for changelog * fragments. * * @param filter the new {@link ArtifactFilter}; may be {@code null} * * @see #getArtifactFilter() * * @see <a * href="http://maven.apache.org/guides/mini/guide-configuring-plugins.html#Mapping_Complex_Objects">Guide * to Configuring Plug-Ins</a> */ public void setArtifactFilter(final ArtifactFilter filter) { this.artifactFilter = filter; } /** * Returns the {@link ArtifactResolver} that will be used internally * to {@linkplain * ArtifactResolver#resolve(ArtifactResolutionRequest) resolve} * {@link Artifact}s representing the {@linkplain #getProject() * current project}'s dependencies. * * <p>This method may return {@code null}.</p> * * @return an {@link ArtifactResolver}, or {@code null} * * @see #setArtifactResolver(ArtifactResolver) * * @see ArtifactResolver#resolve(ArtifactResolutionRequest) */ public ArtifactResolver getArtifactResolver() { return this.artifactResolver; } /** * Sets the {@link ArtifactResolver} that will be used internally * to {@linkplain * ArtifactResolver#resolve(ArtifactResolutionRequest) resolve} * {@link Artifact}s representing the {@linkplain #getProject() * current project}'s dependencies. * * @param resolver the new {@link ArtifactResolver}; may be {@code * null} * * @see #getArtifactResolver() * * @see ArtifactResolver#resolve(ArtifactResolutionRequest) */ public void setArtifactResolver(final ArtifactResolver resolver) { this.artifactResolver = resolver; } /** * Returns a {@link List} of {@link String}s, each element of which * is a name of a {@linkplain ClassLoader#getResource(String) * classpath resource} that might identify an actual changelog * fragment. * * <p>This method may return {@code null}.</p> * * @return a {@link List} of {@link String}s, or {@code null} * * @see #setChangeLogResourceNames(List) * * @see ClassLoader#getResource(String) */ public List<String> getChangeLogResourceNames() { return this.changeLogResourceNames; } /** * Sets the {@link List} of {@link String}s identifying {@linkplain * ClassLoader#getResource(String) classpath resources} that might * identify actual changelog fragments. * * @param changeLogResourceNames a {@link List} of {@link String}s, * each element of which is a name of a {@linkplain * ClassLoader#getResource(String) classpath resource} that might * identify an actual changelog fragment; may be {@code null} * * @see #getChangeLogResourceNames() */ public void setChangeLogResourceNames(final List<String> changeLogResourceNames) { this.changeLogResourceNames = changeLogResourceNames; } /** * Returns the {@linkplain ClassLoader#getResource(String) classpath * resource} name of an <a href="http://mvel.codehaus.org/">MVEL</a> * template that will aggregate changelog fragments together. * * <p>This method may return {@code null}.</p> * * @return the {@linkplain ClassLoader#getResource(String) classpath * resource} name of an <a href="http://mvel.codehaus.org/">MVEL</a> * template that will aggregate changelog fragments together, or * {@code null} * * @see #setChangeLogTemplateResourceName(String) * * @see TemplateCompiler#compileTemplate(String) * * @see ClassLoader#getResource(String) */ public String getChangeLogTemplateResourceName() { return this.changeLogTemplateResourceName; } /** * Sets the {@linkplain ClassLoader#getResource(String) classpath * resource} name of an <a href="http://mvel.codehaus.org/">MVEL</a> * template that will aggregate changelog fragments together. * * @param name the {@linkplain ClassLoader#getResource(String) * classpath resource} name of an <a * href="http://mvel.codehaus.org/">MVEL</a> template that will * aggregate changelog fragments together; may be {@code null} * * @see #getChangeLogTemplateResourceName() * * @see ClassLoader#getResource(String) */ public void setChangeLogTemplateResourceName(final String name) { this.changeLogTemplateResourceName = name; } /** * Returns a {@link String} representing the version of the <a * href="http://www.liquibase.org/">Liquibase</a> {@code * dbchangelog} schema to use. * * <p>This method may return {@code null}.</p> * * @return a {@link String} representing the version of the <a * href="http://www.liquibase.org/">Liquibase</a> {@code * dbchangelog} schema to use, or {@code null} * * @see #setDatabaseChangeLogXsdVersion(String) */ public String getDatabaseChangeLogXsdVersion() { return this.databaseChangeLogXsdVersion; } /** * Sets the {@link String} representing the version of the <a * href="http://www.liquibase.org/">Liquibase</a> {@code * dbchangelog} schema to use. * * @param databaseChangeLogXsdVersion the new version formatted as a * decimal number, hopefully identifying a valid version of the <a * href="http://www.liquibase.org/">Liquibase</a> {@code * dbchangelog} schema; may be {@code null} in which case {@code * 3.0} will be used instead * * @see #getDatabaseChangeLogXsdVersion() */ public void setDatabaseChangeLogXsdVersion(final String databaseChangeLogXsdVersion) { if (databaseChangeLogXsdVersion == null) { this.databaseChangeLogXsdVersion = "3.0"; } else { this.databaseChangeLogXsdVersion = databaseChangeLogXsdVersion; } } /** * Returns a {@link Properties} object containing <a * href="http://www.liquibase.org/documentation/changelog_parameters.html">changelog * parameters</a>. * * <p>This method may return {@code null}.</p> * * @return a {@link Properties} object containing <a * href="http://www.liquibase.org/documentation/changelog_parameters.html">changelog * parameters</a>, or {@code null} * * @see #setChangeLogParameters(Properties) */ public Properties getChangeLogParameters() { return this.changeLogParameters; } /** * Installs a {@link Properties} object containing <a * href="http://www.liquibase.org/documentation/changelog_parameters.html">changelog * parameters</a>. * * @param parameters the changelog parameters to use; may be {@code * null} * * @see #getChangeLogParameters() */ public void setChangeLogParameters(final Properties parameters) { this.changeLogParameters = parameters; } /** * Returns the character encoding used to {@linkplain #write(String, * Collection, File) write the assembled changelog}. * * <p>This method may return {@code null}.</p> * * @return a {@link String} representing a character encoding, or * {@code null} * * @see #setChangeLogCharacterEncoding(String) * * @see <a * href="http://docs.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html#iana">Standard * character encoding names provided by Java SE</a> */ public String getChangeLogCharacterEncoding() { return this.changeLogCharacterEncoding; } /** * Sets the character encoding used to {@linkplain #write(String, * Collection, File) write the assembled changelog}. * * @param encoding a {@link String} representing a character * encoding; may be {@code null} in which case "{@code UTF-8}" will * be used instead * * @see #getChangeLogCharacterEncoding() * * @see <a * href="http://docs.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html#iana">Standard * character encoding names provided by Java SE</a> */ public void setChangeLogCharacterEncoding(final String encoding) { if (encoding == null) { this.changeLogCharacterEncoding = "UTF-8"; } else { this.changeLogCharacterEncoding = encoding; } } /** * Returns the character encoding used to read the <a * href="http://mvel.codehaus.org/">MVEL</a> template used to * aggregate changelog fragments together. * * <p>This method may return {@code null}.</p> * * @return a {@link String} representing a character encoding, or * {@code null} * * @see #setTemplateCharacterEncoding(String) * * @see <a * href="http://docs.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html#iana">Standard * character encoding names provided by Java SE</a> */ public String getTemplateCharacterEncoding() { return this.templateCharacterEncoding; } /** * Sets the character encoding used to read the <a * href="http://mvel.codehaus.org/">MVEL</a> template used to * aggregate changelog fragments together. * * @param encoding a {@link String} representing a character * encoding; may be {@code null} in which case "{@code UTF-8}" will * be used instead * * @see #getTemplateCharacterEncoding() * * @see <a * href="http://docs.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html#iana">Standard * character encoding names provided by Java SE</a> */ public void setTemplateCharacterEncoding(final String encoding) { if (encoding == null) { this.templateCharacterEncoding = "UTF-8"; } else { this.templateCharacterEncoding = encoding; } } /** * Returns the {@link DependencyGraphBuilder} used by this {@link * AssembleChangeLogMojo} to perform dependency resolution. * * <p>This method may return {@code null}.</p> * * @return a {@link DependencyGraphBuilder}, or {@code null} * * @see DependencyGraphBuilder * * @see #setDependencyGraphBuilder(DependencyGraphBuilder) */ public DependencyGraphBuilder getDependencyGraphBuilder() { return this.dependencyGraphBuilder; } /** * Sets the {@link DependencyGraphBuilder} used by this {@link * AssembleChangeLogMojo} to perform dependency resolution. * * @param dependencyGraphBuilder the {@link DependencyGraphBuilder} * to use; must not be {@code null} * * @exception IllegalArgumentException if {@code * dependencyGraphBuilder} is {@code null} * * @see DependencyGraphBuilder * * @see #getDependencyGraphBuilder() */ public void setDependencyGraphBuilder(final DependencyGraphBuilder dependencyGraphBuilder) { if (dependencyGraphBuilder == null) { throw new IllegalArgumentException("dependencyGraphBuilder", new NullPointerException("dependencyGraphBuilder")); } this.dependencyGraphBuilder = dependencyGraphBuilder; } /* * Non-trivial accessors and mutators. */ /** * Examines the {@linkplain #getProject() current * <code>MavenProject</code>}'s dependencies, {@linkplain * #getArtifactResolver() resolving} them if necessary, and * assembles and returns a {@link Collection} whose elements are * {@link URL}s representing reachable changelog fragments that are * classpath resources. * * <p>This method may return {@code null}.</p> * * <p>This method calls the {@link #getChangeLogResources(Iterable)} * method.</p> * * <p>This method invokes the {@link * Artifacts#getArtifactsInTopologicalOrder(MavenProject, * DependencyGraphBuilder, ArtifactFilter, ArtifactResolver, * ArtifactRepository)} method.</p> * * @return a {@link Collection} of {@link URL}s to classpath * resources that are changelog fragments, or {@code null} * * @exception IllegalStateException if the return value of {@link * #getProject()}, {@link #getDependencyGraphBuilder()} or {@link * #getArtifactResolver()} is {@code null} * * @exception ArtifactResolutionException if there was a problem * {@linkplain ArtifactResolver#resolve(ArtifactResolutionRequest) * resolving} a given {@link Artifact} representing a dependency * * @exception DependencyGraphBuilderException if there was a problem * with dependency resolution * * @exception IOException if there was a problem with input or * output * * @see #getChangeLogResources(Iterable) * * @see Artifacts#getArtifactsInTopologicalOrder(MavenProject, * DependencyGraphBuilder, ArtifactFilter, ArtifactResolver, * ArtifactRepository) */ public final Collection<? extends URL> getChangeLogResources() throws ArtifactResolutionException, DependencyGraphBuilderException, IOException { final MavenProject project = this.getProject(); if (project == null) { throw new IllegalStateException("this.getProject()", new NullPointerException("this.getProject()")); } final DependencyGraphBuilder dependencyGraphBuilder = this.getDependencyGraphBuilder(); if (dependencyGraphBuilder == null) { throw new IllegalStateException("this.getDependencyGraphBuilder()", new NullPointerException("this.getDependencyGraphBuilder()")); } final ArtifactResolver resolver = this.getArtifactResolver(); if (resolver == null) { throw new IllegalStateException("this.getArtifactResolver()", new NullPointerException("this.getArtifactResolver()")); } final Collection<? extends Artifact> artifacts = new Artifacts().getArtifactsInTopologicalOrder(project, dependencyGraphBuilder, this.getArtifactFilter(), resolver, this.getLocalRepository()); Collection<? extends URL> urls = null; if (artifacts != null && !artifacts.isEmpty()) { urls = getChangeLogResources(artifacts); } if (urls == null) { urls = Collections.emptySet(); } return urls; } /** * Given an {@link Iterable} of {@link Artifact}s, and given a * non-{@code null}, non-empty return value from the {@link * #getChangeLogResourceNames()} method, this method returns a * {@link Collection} of {@link URL}s representing changelog * {@linkplain ClassLoader#getResources(String) resources found} * among the supplied {@link Artifact}s. * * @param artifacts an {@link Iterable} of {@link Artifact}s; may be * {@code null} * * @return a {@link Collection} of {@link URL}s representing * changelog resources found among the supplied {@link Artifact}s * * @exception IOException if an input/output error occurs such as * the kind that might be thrown by the {@link * ClassLoader#getResources(String)} method * * @see #getChangeLogResourceNames() * * @see ClassLoader#getResources(String) */ public Collection<? extends URL> getChangeLogResources(final Iterable<? extends Artifact> artifacts) throws IOException { final Log log = this.getLog(); Collection<URL> returnValue = null; final ClassLoader loader = this.toClassLoader(artifacts); if (loader != null) { final Iterable<String> changeLogResourceNames = this.getChangeLogResourceNames(); if (log != null && log.isDebugEnabled()) { log.debug(String.format("Change log resource names: %s", changeLogResourceNames)); } if (changeLogResourceNames == null) { throw new IllegalStateException("this.getChangeLogResourceNames()", new NullPointerException("this.getChangeLogResourceNames()")); } returnValue = new ArrayList<URL>(); for (final String name : changeLogResourceNames) { if (name != null) { final Enumeration<URL> urls = loader.getResources(name); if (urls != null) { returnValue.addAll(Collections.list(urls)); } } } } if (returnValue == null) { returnValue = Collections.emptySet(); } return returnValue; } /** * Using an appropriate {@link ClassLoader}, returns a {@link URL} * that may be used to {@linkplain URL#openStream() get} the * changelog template {@linkplain * #getChangeLogTemplateResourceName() associated with} this {@link * AssembleChangeLogMojo}. * * <p>This method may return {@code null}.</p> * * @return a {@link URL} to an <a * href="http://mvel.codehaus.org/">MVEL</a> template, or {@code * null} * * @see #getChangeLogTemplateResourceName() */ public URL getChangeLogTemplateResource() { final String changeLogTemplateResourceName = this.getChangeLogTemplateResourceName(); final String resourceName; final ClassLoader loader; if (changeLogTemplateResourceName == null) { resourceName = "changelog-template.mvl"; loader = this.getClass().getClassLoader(); } else { resourceName = changeLogTemplateResourceName; final ClassLoader candidate = Thread.currentThread().getContextClassLoader(); if (candidate != null) { loader = candidate; } else { loader = this.getClass().getClassLoader(); } } assert loader != null; final URL resource = loader.getResource(resourceName); return resource; } /** * Validates the current {@link File} that represents the full path * to the file that will result after the {@link #execute()} method * runs successfully and returns it. * * <p>This method may invoke {@link File#mkdirs()} and {@link * File#createNewFile()} as part of its operation.</p> * * <p>This method never returns {@code null}.</p> * * @return the {@link File} that represents the full path to the * file that will result after the {@link #execute()} method runs * successfully; never {@code null} * * @exception IllegalStateException if somehow the current {@link * File} {@linkplain File#isDirectory() is a directory} or if it is * somehow {@code null} * * @exception IOException if {@linkplain File#mkdirs() directory * creation} fails or if a new file whose pathname is described by * the current output file could not be {@linkplain * File#createNewFile() created} or if a prior version of the file * existed but could not be {@linkplain File#delete() deleted} */ public File getOutputFile() throws IOException { if (this.outputFile == null) { throw new IllegalStateException("this.outputFile", new NullPointerException("this.outputFile")); } else if (this.outputFile.isDirectory()) { throw new IllegalStateException("this.outputFile.isDirectory()"); } final File parent = this.outputFile.getParentFile(); if (parent != null) { if (!parent.exists() && !parent.mkdirs()) { throw new IOException("Could not create parent directory chain for " + this.outputFile); } } if (this.outputFile.exists()) { if (!this.outputFile.delete()) { throw new IOException("Could not delete extant " + this.outputFile); } if (!this.outputFile.createNewFile()) { throw new IOException( "Could not create a new file corresponding to the path named " + this.outputFile); } } assert this.outputFile != null; assert this.outputFile.exists(); assert !this.outputFile.isDirectory(); return this.outputFile; } /** * Sets the {@link File} that represents the full path to the file * that will result after the {@link #execute()} method runs * successfully. * * @param file the file; if non-{@code null}, then must not be * {@linkplain File#isDirectory() a directory} * * @see #getOutputFile() */ public void setOutputFile(final File file) { if (file != null && file.isDirectory()) { throw new IllegalArgumentException("file", new IOException("file.isDirectory()")); } this.outputFile = file; } /* * Operations. */ /** * Executes this {@link AssembleChangeLogMojo} by calling the {@link * #assembleChangeLog()} method. * * @exception MojoFailureException if an error occurs; under normal * circumstances this goal will not fail so this method does not * throw {@link MojoExecutionException} * * @exception TemplateSyntaxError if the supplied {@code template} * contained syntax errors * * @exception TemplateRuntimeError if there was a problem merging * the supplied {@link Collection} of {@link URL}s with the compiled * version of the supplied {@code template} * * @see #assembleChangeLog() */ @Override public void execute() throws MojoFailureException { final Log log = this.getLog(); if (this.getSkip()) { if (log != null && log.isDebugEnabled()) { log.debug("Skipping execution by request"); } } else { try { this.assembleChangeLog(); } catch (final RuntimeException e) { throw e; } catch (final IOException e) { throw new MojoFailureException("Failure assembling changelog", e); } catch (final ArtifactResolutionException e) { throw new MojoFailureException("Failure assembling changelog", e); } catch (final DependencyGraphBuilderException e) { throw new MojoFailureException("Failure assembling changelog", e); } } } /** * Assembles a <a href="http://www.liquibase.org/">Liquibase</a> <a * href="http://www.liquibase.org/documentation/databasechangelog.html">changelog</a> * from changelog fragments {@linkplain #getChangeLogResources() * found in topological dependency order among the dependencies} of * the {@linkplain #getProject() current project}. * * <p>This method:</p> * * <ul> * * <li>{@linkplain #getChangeLogTemplateResource() Verifies that * there is a template} that exists either in the {@linkplain * #getProject() project} or (more commonly) in this plugin</li> * * <li>Verifies that the template can be read and has contents</li> * * <li>{@linkplain * Artifacts#getArtifactsInTopologicalOrder(MavenProject, * DependencyGraphBuilder, ArtifactFilter, ArtifactResolver, * ArtifactRepository) Retrieves and resolves the project's * dependencies and sorts them in topological order} from the * artifact with the least dependencies to {@linkplain #getProject() * the current project} (which by definition has the most * dependencies)</li> * * <li>Builds a {@link ClassLoader} that can "see" the {@linkplain * Artifact#getFile() <code>File</code>s associated with those * <code>Artifact</code>s} and uses it to {@linkplain * ClassLoader#getResources(String) find} the {@linkplain * #getChangeLogResourceNames() specified changelog resources}</li> * * <li>Passes a {@link Collection} of {@link URL}s representing (in * most cases) {@code file:} or {@code jar:} {@link URL}s through * the {@linkplain TemplateRuntime MVEL template engine}, thus * merging the template and the {@link URL}s into an aggregating * changelog</li> * * <li>{@linkplain #write(String, Collection, File) Writes} the * resulting changelog to the destination denoted by the {@link * #getOutputFile() outputFile} parameter</li> * * </ul> * * @exception ArtifactResolutionException if there was a problem * {@linkplain ArtifactResolver#resolve(ArtifactResolutionRequest) * resolving} a given {@link Artifact} representing a dependency * * @exception DependencyGraphBuilderException if there was a problem * with dependency resolution * * @exception IOException if there was a problem with input or * output * * @see #getChangeLogTemplateResource() * * @see #getChangeLogResources() * * @see #getOutputFile() * * @see #write(String, Collection, File) */ public final void assembleChangeLog() throws ArtifactResolutionException, DependencyGraphBuilderException, IOException { final Log log = this.getLog(); final URL changeLogTemplateResource = this.getChangeLogTemplateResource(); if (log != null && log.isDebugEnabled()) { log.debug(String.format("Change log template resource: %s", changeLogTemplateResource)); } if (changeLogTemplateResource != null) { final String templateContents = this.readTemplate(changeLogTemplateResource); if (log != null && log.isDebugEnabled()) { log.debug(String.format("Change log template contents: %s", templateContents)); } if (templateContents != null) { final Collection<? extends URL> urls = this.getChangeLogResources(); if (log != null && log.isDebugEnabled()) { log.debug(String.format("Change log resources: %s", urls)); } if (urls != null && !urls.isEmpty()) { final File outputFile = this.getOutputFile(); if (log != null && log.isDebugEnabled()) { log.debug(String.format("Output file: %s", outputFile)); } if (outputFile != null) { this.write(templateContents, urls, outputFile); } } } } } /** * Writes appropriate representations of the supplied {@link URL}s * as interpreted and merged into the supplied {@code template} * contents to the {@link File} represented by the {@code * outputFile} parameter value. * * @param template an <a href="http://mvel.codehaus.org/">MVEL</a> * template; may be {@code null} in which case no action will be * taken * * @param urls a {@link Collection} of {@link URL}s representing * existing changelog fragment resources, sorted in topological * dependency order; may be {@code null} in which case no action * will be taken * * @param outputFile a {@link File} representing the full path to * the location where an aggregate changelog should be written; may * be {@code null} in which case no action will be taken; not * validated in any way by this method * * @exception IOException if there was a problem writing to the supplied {@link File} * * @exception TemplateSyntaxError if the supplied {@code template} * contained syntax errors * * @exception TemplateRuntimeError if there was a problem merging * the supplied {@link Collection} of {@link URL}s with the compiled * version of the supplied {@code template} * * @see #getOutputFile() * * @see #getChangeLogResources() * * @see #getChangeLogResourceNames() */ public void write(final String template, final Collection<? extends URL> urls, final File outputFile) throws IOException { if (template != null && urls != null && !urls.isEmpty() && outputFile != null) { final CompiledTemplate compiledTemplate = TemplateCompiler.compileTemplate(template); assert compiledTemplate != null; final Map<Object, Object> variables = new HashMap<Object, Object>(); variables.put("databaseChangeLogXsdVersion", this.getDatabaseChangeLogXsdVersion()); variables.put("changeLogParameters", this.getChangeLogParameters()); variables.put("resources", urls); String encoding = this.getChangeLogCharacterEncoding(); if (encoding == null) { encoding = "UTF-8"; } final Log log = this.getLog(); if (log != null && log.isDebugEnabled()) { log.debug(String.format("Writing change log to %s using character encoding %s", outputFile, encoding)); } final Writer writer = new BufferedWriter( new OutputStreamWriter(new FileOutputStream(outputFile), encoding)); try { TemplateRuntime.execute(compiledTemplate, this, new MapVariableResolverFactory(variables), null /* no TemplateRegistry */, new TemplateOutputWriter(writer)); } finally { try { writer.flush(); } catch (final IOException ignore) { // ignore on purpose } try { writer.close(); } catch (final IOException ignore) { // ignore on purpose } } } } /** * Given a {@link URL} to a changelog template, fully reads that * template into memory and returns it, uninterpolated, as a {@link * String}. * * <p>This method may return {@code null}.</p> * * @param changeLogTemplateResource a {@link URL} to an <a * href="http://mvel.codehaus.org/">MVEL<a> template; must not be * {@code null} * * @return the contents of the template, uninterpolated, or {@code * null} * * @exception IOException if an input/output error occurs * * @see #getTemplateCharacterEncoding() */ private final String readTemplate(final URL changeLogTemplateResource) throws IOException { final Log log = this.getLog(); if (changeLogTemplateResource == null) { throw new IllegalArgumentException("changeLogTemplateResource", new NullPointerException("changeLogTemplateResource")); } String returnValue = null; final InputStream rawStream = changeLogTemplateResource.openStream(); if (rawStream != null) { BufferedReader reader = null; String templateCharacterEncoding = this.getTemplateCharacterEncoding(); if (templateCharacterEncoding == null) { templateCharacterEncoding = "UTF-8"; } if (log != null && log.isDebugEnabled()) { log.debug(String.format("Reading change log template from %s using character encoding %s", changeLogTemplateResource, templateCharacterEncoding)); } try { reader = new BufferedReader(new InputStreamReader(rawStream, templateCharacterEncoding)); String line = null; final StringBuilder sb = new StringBuilder(); while ((line = reader.readLine()) != null) { sb.append(line); sb.append(LS); } returnValue = sb.toString(); } finally { try { rawStream.close(); } catch (final IOException nothingWeCanDo) { } if (reader != null) { try { reader.close(); } catch (final IOException nothingWeCanDo) { } } } } else if (log != null && log.isDebugEnabled()) { log.debug(String.format("Opening change log template %s results in a null InputStream.", changeLogTemplateResource)); } return returnValue; } /** * Creates and returns a new {@link ClassLoader} whose classpath * encompasses reachable changelog resources found among the * supplied {@link Artifact}s. * * <p>This method may return {@code null}.</p> * * @param artifacts an {@link Iterable} of {@link Artifact}s, some * of whose members may house changelog fragments; may be {@code * null} in which case {@code null} will be returned * * @return an appropriate {@link ClassLoader}, or {@code null} * * @exception MalformedURLException if during classpath assembly a * bad {@link URL} was encountered * * @see #toURLs(Artifact) */ private final ClassLoader toClassLoader(final Iterable<? extends Artifact> artifacts) throws MalformedURLException { final Log log = this.getLog(); ClassLoader loader = null; if (artifacts != null) { final Collection<URL> urls = new ArrayList<URL>(); for (final Artifact artifact : artifacts) { final Collection<? extends URL> classpathElements = this.toURLs(artifact); if (classpathElements != null && !classpathElements.isEmpty()) { urls.addAll(classpathElements); } } if (!urls.isEmpty()) { if (log != null && log.isDebugEnabled()) { log.debug(String.format("Creating URLClassLoader with the following classpath: %s", urls)); } loader = new URLClassLoader(urls.toArray(new URL[urls.size()]), Thread.currentThread().getContextClassLoader()); } } return loader; } /** * Returns a {@link Collection} of {@link URL}s representing the * locations of the given {@link Artifact}. * * <p>This method never returns {@code null}.</p> * * <h4>Design Notes</h4> * * <p>This method returns a {@link Collection} of {@link URL}s * instead of a single {@link URL} because an {@link Artifact} * representing the {@linkplain #getProject() current project being * built} has two conceptual locations for our purposes: the test * output directory and the build output directory. All other * {@link Artifact}s have exactly one location, <em>viz.</em> {@link * Artifact#getFile()}.</p> * * @param artifact the {@link Artifact} for which {@link URL}s * should be returned; may be {@code null} in which case an * {@linkplain Collection#emptySet() empty <code>Collection</code>} * will be returned * * @return a {@link Collection} of {@link URL}s; never {@code null} * * @exception MalformedURLException if an {@link Artifact}'s * {@linkplain Artifact#getFile() associated <code>File</code>} * could not be {@linkplain URI#toURL() converted into a * <code>URL</code>} * * @see Artifact#getFile() * * @see Build#getTestOutputDirectory() * * @see Build#getOutputDirectory() * * @see File#toURI() * * @see URI#toURL() */ private final Collection<? extends URL> toURLs(final Artifact artifact) throws MalformedURLException { Collection<URL> urls = null; if (artifact != null) { // If the artifact represents the current project itself, then // we need to look in the reactor first (i.e. the // project.build.testOutpuDirectory and the // project.build.outputDirectory areas), since a .jar file for // the project in all likelihood has not yet been created. final String groupId = artifact.getGroupId(); if (groupId != null) { final MavenProject project = this.getProject(); if (project != null && groupId.equals(project.getGroupId())) { final String artifactId = artifact.getArtifactId(); if (artifactId != null && artifactId.equals(project.getArtifactId())) { final Build build = project.getBuild(); if (build != null) { urls = new ArrayList<URL>(); urls.add(new File(build.getTestOutputDirectory()).toURI().toURL()); urls.add(new File(build.getOutputDirectory()).toURI().toURL()); } } } } // If on the other hand the artifact was just a garden-variety // direct or transitive dependency, then just add its file: URL // directly. if (urls == null) { final File file = artifact.getFile(); if (file != null) { final URI uri = file.toURI(); if (uri != null) { urls = Collections.singleton(uri.toURL()); } } } } if (urls == null) { urls = Collections.emptySet(); } return urls; } }