Package com.edugility.liquibase.maven

Source Code of com.edugility.liquibase.maven.AssembleChangeLogMojo

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

}
TOP

Related Classes of com.edugility.liquibase.maven.AssembleChangeLogMojo

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.