001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright (c) 2014 Edugility LLC.
004 *
005 * Permission is hereby granted, free of charge, to any person
006 * obtaining a copy of this software and associated documentation
007 * files (the "Software"), to deal in the Software without
008 * restriction, including without limitation the rights to use, copy,
009 * modify, merge, publish, distribute, sublicense and/or sell copies
010 * of the Software, and to permit persons to whom the Software is
011 * furnished to do so, subject to the following conditions:
012 *
013 * The above copyright notice and this permission notice shall be
014 * included in all copies or substantial portions of the Software.
015 *
016 * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
017 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
018 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
019 * NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
020 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
021 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
022 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
023 * DEALINGS IN THE SOFTWARE.
024 *
025 * The original copy of this license is available at
026 * http://www.opensource.org/license/mit-license.html.
027 */
028package com.edugility.maven.liquibase;
029
030import java.io.File;
031import java.io.IOException;
032
033import java.net.MalformedURLException;
034import java.net.URL;
035import java.net.URLClassLoader;
036
037import java.util.ArrayList;
038import java.util.Collection;
039import java.util.Collections;
040
041import com.edugility.maven.ArtifactsProcessingException;
042import com.edugility.maven.ArtifactsProcessor;
043
044import org.apache.maven.artifact.Artifact;
045
046import org.apache.maven.plugin.logging.Log;
047
048import org.apache.maven.model.Build;
049
050import org.apache.maven.project.MavenProject;
051
052/**
053 * An {@link ArtifactsProcessor} for use in conjunction with the <a
054 * href="http://ljnelson.github.io/artifact-maven-plugin/index.html"><code>artifact-maven-plugin</code></a>
055 * that creates a <a href="http://www.liquibase.org/">Liquibase</a> <a
056 * href="http://www.liquibase.org/documentation/databasechangelog.html">changelog</a>
057 * <a
058 * href="http://www.liquibase.org/documentation/include.html">aggregating</a>
059 * changelog fragments found in a {@link MavenProject}'s dependencies.
060 *
061 * @author <a href="http://about.me/lairdnelson"
062 * target="_parent">Laird Nelson</a>
063 *
064 * @see <a
065 * href="http://ljnelson.github.io/artifact-maven-plugin/apidocs/index.html"
066 * target="_parent">The documentation for the
067 * artifact-maven-plugin</a>
068 */
069public class LiquibaseChangeLogArtifactsProcessor implements ArtifactsProcessor {
070
071
072  /*
073   * Instance fields.
074   */
075
076
077  /**
078   * The names of Liquibase changelog fragments to be sought.
079   *
080   * <p>This field may be {@code null}.</p>
081   *
082   * @see #getChangeLogResourceNames()
083   *
084   * @see #setChangeLogResourceNames(Collection)
085   */
086  private Collection<String> changeLogResourceNames;
087
088  /**
089   * The {@link AggregateChangeLogGenerator} to use to perform
090   * changelog generation.
091   *
092   * <p>This field may be {@code null}.</p>
093   *
094   * @see #getChangeLogGenerator()
095   *
096   * @see #setChangeLogGenerator(AggregateChangeLogGenerator)
097   */
098  private AggregateChangeLogGenerator changeLogGenerator;
099
100
101  /*
102   * Constructors.
103   */
104
105
106  /**
107   * Creates a new {@link LiquibaseChangeLogArtifactsProcessor}.
108   */
109  public LiquibaseChangeLogArtifactsProcessor() {
110    super();
111    this.setChangeLogGenerator(new AggregateChangeLogGenerator());
112    this.setChangeLogResourceNames(Collections.singleton("META-INF/liquibase/changelog.xml"));
113  }
114
115
116  /*
117   * Properties.
118   */
119
120
121  /**
122   * Returns the {@link AggregateChangeLogGenerator} to be used to
123   * generate changelogs.
124   *
125   * <p>This method may return {@code null}.</p>
126   *
127   * @return an {@link AggregateChangeLogGenerator}, or {@code null}
128   *
129   * @see #setChangeLogGenerator(AggregateChangeLogGenerator)
130   */
131  public AggregateChangeLogGenerator getChangeLogGenerator() {
132    return this.changeLogGenerator;
133  }
134
135  /**
136   * Sets the {@link AggregateChangeLogGenerator} to be used to
137   * generate changelogs.
138   *
139   * @param changeLogGenerator the new generator to use; may be {@code
140   * null} in which case a new {@link AggregateChangeLogGenerator}
141   * will be used internally instead
142   *
143   * @see #getChangeLogGenerator()
144   */
145  public void setChangeLogGenerator(final AggregateChangeLogGenerator changeLogGenerator) {
146    this.changeLogGenerator = changeLogGenerator;
147  }
148
149  /**
150   * Returns the relative names of resources representing Liquibase
151   * changelog fragments that this {@link
152   * LiquibaseChangeLogArtifactsProcessor} will look for.
153   *
154   * <p>This method may return {@code null}.</p>
155   *
156   * <p>Typically, this method returns a singleton {@link Collection}
157   * containing the text {@code META-INF/liquibase/changelog.xml}.</p>
158   *
159   * @return a {@link Collection} of relative resource names
160   * representing Liquibase changelog fragments, or {@code null}
161   *
162   * @see #setChangeLogResourceNames(Collection)
163   */
164  public Collection<String> getChangeLogResourceNames() {
165    return this.changeLogResourceNames;
166  }
167
168  /**
169   * Sets the {@link Collection} of relative resource names
170   * representing Liquibase changelog fragments that this {@link
171   * LiquibaseChangeLogArtifactsProcessor} will look for.
172   *
173   * @param changeLogResourceNames the names; may be {@code null}
174   *
175   * @see #getChangeLogResourceNames()
176   */
177  public void setChangeLogResourceNames(final Collection<String> changeLogResourceNames) {
178    this.changeLogResourceNames = changeLogResourceNames;
179  }
180
181
182  /*
183   * ArtifactsProcessor implementation.
184   */
185
186
187  /**
188   * Harvests {@code jar:} {@link URL}s from the supplied resolved
189   * {@link Artifact}s and lists them in topological order as {@code
190   * <include>} elements inside a generated Liquibase changelog file.
191   *
192   * <p>This method never returns {@code null}.</p>
193   *
194   * @param project the {@link MavenProject} currently in effect; will
195   * not be {@code null}
196   *
197   * @param artifacts a {@link Collection} of {@link Artifact}s
198   * representing the full, transitive set of resolved dependencies of
199   * the supplied {@link MavenProject}; will not be {@code null}
200   *
201   * @param log a {@link Log} for logging to a Maven console; may be
202   * {@code null}
203   *
204   * @return the supplied artifacts
205   *
206   * @exception ArtifactsProcessingException if an error occurs
207   *
208   * @see ArtifactsProcessor
209   */
210  @Override
211  public Collection<? extends Artifact> process(final MavenProject project, final Collection<? extends Artifact> artifacts, final Log log) throws ArtifactsProcessingException {
212    final Collection<? extends URL> changeLogUrls = this.gatherUrls(project, artifacts, log);
213    if (changeLogUrls != null && !changeLogUrls.isEmpty()) {
214      this.generateChangeLog(project, changeLogUrls, log);
215    }
216    return artifacts;
217  }
218
219
220  /*
221   * Private methods.
222   */
223
224
225  private final Collection<? extends URL> gatherUrls(final MavenProject project, final Collection<? extends Artifact> artifacts, final Log log) throws ArtifactsProcessingException {
226    final Collection<? extends URL> artifactUrls = this.gatherArtifactUrls(project, artifacts, log);
227    final int artifactUrlsSize = artifactUrls == null ? 0 : artifactUrls.size();
228
229    final Collection<? extends URL> projectUrls = this.gatherProjectUrls(project, log);
230    final int projectUrlsSize = projectUrls == null ? 0 : projectUrls.size();
231
232    final Collection<URL> returnValue = new ArrayList<URL>(artifactUrlsSize + projectUrlsSize);
233    if (artifactUrlsSize > 0) {
234      returnValue.addAll(artifactUrls);
235    }
236    if (projectUrlsSize > 0) {
237      returnValue.addAll(projectUrls);
238    }
239    return returnValue;
240  }
241
242  private final Collection<? extends URL> gatherArtifactUrls(final MavenProject project, final Collection<? extends Artifact> artifacts, final Log log) throws ArtifactsProcessingException {
243    Collection<URL> returnValue = null;
244    if (artifacts != null && !artifacts.isEmpty()) {
245      final Collection<? extends String> names = this.getChangeLogResourceNames();
246      if (names != null && !names.isEmpty()) {
247        for (final Artifact artifact : artifacts) {
248          if (artifact != null && artifact.isResolved() && (project == null || !artifact.equals(project.getArtifact()))) {
249            final File artifactFile = artifact.getFile();
250            if (artifactFile != null && artifactFile.canRead()) {
251              for (final String name : names) {
252                if (name != null) {
253                  URL url = null;
254                  try {
255                    url = artifactFile.toURI().toURL();
256                  } catch (final MalformedURLException wrapMe) {
257                    throw new ArtifactsProcessingException(wrapMe);
258                  }
259                  final URLClassLoader loader = new URLClassLoader(new URL[] { url }, Thread.currentThread().getContextClassLoader());
260                  final URL jarUrlToChangeLog = loader.getResource(name);
261                  if (jarUrlToChangeLog != null) {
262                    if (returnValue == null) {
263                      returnValue = new ArrayList<URL>(artifacts.size() * names.size());
264                    }
265                    returnValue.add(jarUrlToChangeLog);
266                  }
267                }
268              }
269            }
270          }
271        }
272      }
273    }
274    return returnValue;
275  }
276
277  private final Collection<? extends URL> gatherProjectUrls(final MavenProject project, final Log log) throws ArtifactsProcessingException {
278    Collection<URL> urls = null;
279    if (project != null) {
280      final Build build = project.getBuild();
281      if (build != null) {
282        final Collection<? extends String> names = this.getChangeLogResourceNames();
283        if (names != null && !names.isEmpty()) {
284          final String[] directoryNames = new String[] { build.getOutputDirectory(), build.getTestOutputDirectory() };
285          for (final String directoryName : directoryNames) {
286            if (directoryName != null) {
287              final File directory = new File(directoryName);
288              if (directory.isDirectory()) {
289                for (final String resourceName : names) {
290                  if (resourceName != null) {
291                    final File changeLogFile = new File(directory, resourceName);
292                    if (changeLogFile.isFile() && changeLogFile.canRead()) {
293                      if (urls == null) {
294                        urls = new ArrayList<URL>(2 * names.size());
295                      }
296                      try {
297                        urls.add(changeLogFile.toURI().toURL());
298                      } catch (final MalformedURLException wrapMe) {
299                        throw new ArtifactsProcessingException(wrapMe);
300                      }
301                    }
302                  }
303                }
304              }
305            }
306          }
307        }
308      }
309    }
310    return urls;
311  }
312
313  private final File generateChangeLog(final MavenProject project, final Collection<? extends URL> urls, final Log log) throws ArtifactsProcessingException {
314    File returnValue = null;
315    if (urls != null && !urls.isEmpty()) {
316      AggregateChangeLogGenerator generator = this.getChangeLogGenerator();
317      if (generator == null) {
318        generator = new AggregateChangeLogGenerator();
319      }
320      final File changeLogFile = generator.getAggregateChangeLogFile();
321      if (changeLogFile != null) {
322        final File parent = changeLogFile.getParentFile();
323        if (parent != null) {
324          parent.mkdirs();
325        }
326      }
327      try {
328        returnValue = generator.generate(urls);
329      } catch (final IOException wrapMe) {
330        throw new ArtifactsProcessingException(wrapMe);
331      }
332    }
333    return returnValue;
334  }
335
336}