com.edugility.liquibase.maven.AssembleChangeLogMojo.java Source code

Java tutorial

Introduction

Here is the source code for com.edugility.liquibase.maven.AssembleChangeLogMojo.java

Source

/* -*- 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;
    }

}