RTR.java

/*
 * Copyright (C) 2016 Ronald Jack Jenkins Jr.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package info.ronjenkins.maven.rtr;

import info.ronjenkins.maven.rtr.exceptions.SmartReactorSanityCheckException;
import info.ronjenkins.maven.rtr.steps.SmartReactorStep;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.maven.AbstractMavenLifecycleParticipant;
import org.apache.maven.MavenExecutionException;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.graph.DefaultProjectDependencyGraph;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.ProjectBuilder;
import org.codehaus.plexus.PlexusContainer;
import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.component.annotations.Requirement;
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
import org.codehaus.plexus.logging.Logger;

/**
 * The entry point for the Smart Reactor Maven Extension.
 *
 * @author Ronald Jack Jenkins Jr.
 */
@Component(role = AbstractMavenLifecycleParticipant.class, hint = "rtr")
public class RTR extends AbstractMavenLifecycleParticipant {
  private static void checkForRequiredClasses() {
    try {
      new DefaultProjectDependencyGraph(new ArrayList<MavenProject>());
      throw new Exception();
    }
    catch (final Exception e) {
      // Irrelevant.
    }
  }

  @Requirement
  private PlexusContainer                 container;
  @Requirement
  private Logger                          logger;
  @Requirement
  private ProjectBuilder                  builder;
  protected List<String>                  startSteps;
  protected List<String>                  endSuccessSteps;
  protected List<String>                  endFailureSteps;
  @Requirement(role = SmartReactorStep.class)
  protected Map<String, SmartReactorStep> availableSteps;
  private RTRComponents                   components;
  private boolean                         disabledDueToDoubleLoad;
  private boolean                         disabled;
  private boolean                         release;
  private boolean                         backupPomsCreated;
  private boolean                         externalSnapshotsAllowed;

  /**
   * RTR entry point.
   *
   * @param session
   *          the current Maven session, never null.
   */
  @Override
  public void afterProjectsRead(final MavenSession session)
      throws MavenExecutionException {
    // Don't allow this extension to be loaded as a build extension.
    try {
      RTR.checkForRequiredClasses();
    }
    catch (final NoClassDefFoundError e) {
      throw new SmartReactorSanityCheckException(
          "This extension must be loaded as a core extension, not as a build extension.");
    }
    // Don't allow double-execution due to double-classloading.
    this.detectDoubleExecution(session);
    if (this.disabledDueToDoubleLoad) {
      return;
    }
    // Don't do anything if the Smart Reactor is disabled.
    final MavenProject executionRoot = session.getTopLevelProject();
    this.disabled = RTRConfig.isDisabled(session, executionRoot);
    if (this.disabled) {
      return;
    }
    this.release = RTRConfig.isRelease(session, executionRoot);
    this.externalSnapshotsAllowed = RTRConfig.isExternalSnapshotsAllowed(
        session, executionRoot);
    this.logger.info("Assembling smart reactor...");
    this.components = new RTRComponents(this.builder);
    this.executeSteps(this.startSteps, session, this.components);
    // Done. Maven build will proceed from here, none the wiser. ;)
  }

  @Override
  public void afterSessionEnd(final MavenSession session)
      throws MavenExecutionException {
    // Don't allow double-execution due to double-classloading.
    if (this.disabledDueToDoubleLoad) {
      return;
    }
    if (this.disabled) {
      return;
    }
    if (session.getResult().hasExceptions()) {
      this.executeSteps(this.endFailureSteps, session, this.components);
    }
    else {
      this.executeSteps(this.endSuccessSteps, session, this.components);
    }
  }

  private void detectDoubleExecution(final MavenSession session)
      throws SmartReactorSanityCheckException {
    // Get the list of core extensions.
    final List<AbstractMavenLifecycleParticipant> extensions;
    try {
      extensions = this.getExtensions(session);
    }
    catch (final ComponentLookupException e) {
      this.logger.error(ExceptionUtils.getFullStackTrace(e));
      throw new SmartReactorSanityCheckException(
          "Error while checking extension classloaders. Please report this as a bug.");
    }
    // If we find this FQCN more than once, "this" is a double-loaded instance
    // of the libext extension. Disable it so it doesn't cause a failure.
    final String thisFqcn = this.getClass().getName();
    boolean found = false;
    for (final AbstractMavenLifecycleParticipant extension : extensions) {
      if (extension.getClass().getName().equals(thisFqcn)) {
        if (found) {
          // Found twice. Stop searching.
          this.disabledDueToDoubleLoad = true;
          break;
        }
        else {
          // Found once.
          found = true;
        }
      }
    }
  }

  private void executeSteps(final List<String> steps,
      final MavenSession session, final RTRComponents components)
      throws MavenExecutionException {
    SmartReactorStep step;
    for (final String name : steps) {
      step = this.availableSteps.get(name);
      if (step == null) {
        throw new MavenExecutionException("Unable to find step '" + name
            + "' to execute", new IllegalStateException());
      }
      step.execute(session, components);
    }
  }

  // Factored into a separate method for mocking purposes, since JMockit can't
  // mock ClassLoader.
  private List<AbstractMavenLifecycleParticipant> getExtensions(
      final MavenSession session) throws ComponentLookupException {
    final List<AbstractMavenLifecycleParticipant> mvnExtensionsXml = new ArrayList<>();
    // Save the original classloader.
    final ClassLoader originalClassLoader = Thread.currentThread()
        .getContextClassLoader();
    try {
      // Get the libext extensions.
      ClassLoader plexusCore = this.getClass().getClassLoader();
      while (plexusCore.getParent() != null) {
        plexusCore = plexusCore.getParent();
      }
      Thread.currentThread().setContextClassLoader(plexusCore);
      mvnExtensionsXml.addAll(this.container
          .lookupList(AbstractMavenLifecycleParticipant.class));
      // Get the .mvn/extensions.xml extensions.
      for (final MavenProject project : session.getProjects()) {
        // getClassRealm() is not considered part of Maven's public API for
        // plugins, but no mention is made of extensions. It's the only reliable
        // way to see which extensions are loaded for each project, so we'll use
        // it as long as we can get away with it.
        final ClassLoader projectRealm = project.getClassRealm();
        if (projectRealm != null) {
          Thread.currentThread().setContextClassLoader(projectRealm);
          mvnExtensionsXml.addAll(this.container
              .lookupList(AbstractMavenLifecycleParticipant.class));
        }
      }
    }
    finally {
      // Restore the original classloader.
      Thread.currentThread().setContextClassLoader(originalClassLoader);
    }
    return mvnExtensionsXml;
  }

  /**
   * Indicates whether or not backup POMs were created by the release process.
   *
   * @return backupPomsCreated true if backup POMs have been created, false
   *         otherwise.
   */
  public boolean isBackupPomsCreated() {
    return this.backupPomsCreated;
  }

  /**
   * Indicates whether or not the Smart Reactor should allow a release reactor
   * containing references to any non-reactor SNAPSHOT artifacts.
   *
   * @return true if allowed, false if prohibited.
   */
  public boolean isExternalSnapshotsAllowed() {
    return this.externalSnapshotsAllowed;
  }

  /**
   * Indicates whether or not a release was requested.
   *
   * @return true if a release was requested, false otherwise.
   */
  public boolean isRelease() {
    return this.release;
  }

  /**
   * Sets the flag that indicates whether or not backup POMs were created by the
   * release process.
   *
   * @param backupPomsCreated
   *          true if backup POMs have been created, false otherwise.
   */
  public void setBackupPomsCreated(final boolean backupPomsCreated) {
    this.backupPomsCreated = backupPomsCreated;
  }
}