PackageMojo.java

/*
 * #%L
 * wcm.io
 * %%
 * Copyright (C) 2015 wcm.io
 * %%
 * 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.
 * #L%
 */
package io.wcm.devops.conga.tooling.maven.plugin;

import static io.wcm.devops.conga.tooling.maven.plugin.BuildConstants.CLASSIFIER_CONFIGURATION;
import static io.wcm.devops.conga.tooling.maven.plugin.BuildConstants.FILE_EXTENSION_CONFIGURATION;
import static io.wcm.devops.conga.tooling.maven.plugin.BuildConstants.PACKAGING_CONFIGURATION;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.archiver.Archiver;
import org.codehaus.plexus.archiver.ArchiverException;
import org.codehaus.plexus.archiver.zip.ZipArchiver;

import io.wcm.devops.conga.generator.util.FileUtil;

/**
 * Packages the generated configurations in a ZIP file.
 */
@Mojo(name = "package", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true, threadSafe = true)
public class PackageMojo extends AbstractCongaMojo {

  /**
   * Selected environments to generate.
   */
  @Parameter(property = "conga.environments")
  private String[] environments;

  /**
   * If set to true (default) a separate ZIP artifact is generated per environment.
   * Otherwise a single ZIP containing all environments in sub directories is created.
   */
  @Parameter(defaultValue = "true")
  private boolean artifactPerEnvironment;

  @Component(role = Archiver.class, hint = "zip")
  private ZipArchiver zipArchiver;

  @Override
  @SuppressWarnings("PMD.UseStringBufferForStringAppends")
  public void execute() throws MojoExecutionException, MojoFailureException {

    // build attachments with all generated configurations
    buildGeneratedConfigurationAttachments();

  }


  @SuppressWarnings({
      "PMD.UseStringBufferForStringAppends",
      "java:S3776" // ignore complexity
  })
  private void buildGeneratedConfigurationAttachments() throws MojoExecutionException, MojoFailureException {
    Set<String> selectedEnvironments;
    if (environments != null && environments.length > 0) {
      selectedEnvironments = Set.copyOf(Arrays.asList(environments));
    }
    else {
      selectedEnvironments = null;
    }

    // collect configuration environment directories
    File configRootDir = getTargetDir();
    List<File> environmentDirs = Arrays.stream(configRootDir.listFiles())
        .filter(File::isDirectory)
        .filter(dir -> selectedEnvironments == null || selectedEnvironments.contains(dir.getName()))
        .collect(Collectors.toList());

    MavenProject project = getProject();
    if (artifactPerEnvironment) {
      // generate an ZIP artifact with generated configurations for each environment
      for (File environmentDir : environmentDirs) {

        // classifier is environment name
        // if current project is not a config project, prefix the classifier
        String classifier = environmentDir.getName();
        if (!StringUtils.equals(project.getPackaging(), PACKAGING_CONFIGURATION)) {
          classifier = CLASSIFIER_CONFIGURATION + "-" + classifier;
        }
        validateClassifier(classifier);

        // build ZIP artifact
        File outputFile = buildZipFile(environmentDir, classifier);

        // attach ZIP artifact
        projectHelper.attachArtifact(project, outputFile, classifier);

      }

      // additionally build a JAR file with all CONGA definitions and resources as main artifact
      buildDefinitionsJarFile();

    }
    else {
      // generate an ZIP artifact containing all environments
      String classifier = null;
      if (!StringUtils.equals(project.getPackaging(), PACKAGING_CONFIGURATION)) {
        classifier = CLASSIFIER_CONFIGURATION;
      }
      validateClassifier(classifier);

      File outputFile = buildZipFile(configRootDir, classifier);
      // set or attach ZIP artifact
      if (StringUtils.equals(project.getPackaging(), PACKAGING_CONFIGURATION)) {
        project.getArtifact().setFile(outputFile);
      }
      else {
        projectHelper.attachArtifact(project, outputFile, CLASSIFIER_CONFIGURATION);
      }
    }

  }

  /**
   * Build JAR file with definitions.
   * @param contentDirectory Content directory for JAR file
   * @return JAR file
   */
  private File buildZipFile(File contentDirectory, String classifier) throws MojoExecutionException {
    File zipFile = new File(getProject().getBuild().getDirectory(), buildZipFileName(classifier));

    String basePath = toZipDirectoryPath(contentDirectory);
    addZipDirectory(basePath, contentDirectory);
    zipArchiver.setDestFile(zipFile);
    try {
      zipArchiver.createArchive();
    }
    catch (ArchiverException | IOException ex) {
      throw new MojoExecutionException("Unable to build file " + zipFile.getPath() + ": " + ex.getMessage(), ex);
    }

    return zipFile;
  }

  /**
   * Recursive through all directory and add file to zipArchiver.
   * This is used instead of zipArchiver.addDirectory to make sure for symlinks the target of the symlink
   * are included rather than the symlink information itself.
   * @param basePath Base path
   * @param directory Directory to include
   */
  @SuppressWarnings("java:S3776") // ignore complexity
  private void addZipDirectory(String basePath, File directory) throws MojoExecutionException {
    String directoryPath = toZipDirectoryPath(directory);
    if (StringUtils.startsWith(directoryPath, basePath)) {
      String relativeDirectoryPath = StringUtils.substring(directoryPath, basePath.length());
      File[] files = directory.listFiles();
      if (files != null) {
        for (File file : files) {
          if (file.isDirectory()) {
            addZipDirectory(basePath, file);
          }
          else if (Files.isSymbolicLink(file.toPath())) {
            // include file symlink is pointing at
            try {
              zipArchiver.addFile(file.toPath().toRealPath().toFile(), relativeDirectoryPath + file.getName());
            }
            catch (IOException ex) {
              throw new MojoExecutionException("Unable to include symlinked file " + FileUtil.getCanonicalPath(file), ex);
            }
          }
          else {
            zipArchiver.addFile(file, relativeDirectoryPath + file.getName());
          }
        }
      }
    }
  }

  private String toZipDirectoryPath(File directory) {
    String canoncialPath = FileUtil.getCanonicalPath(directory);
    return StringUtils.replace(canoncialPath, "\\", "/") + "/";
  }

  private String buildZipFileName(String classifier) {
    StringBuilder sb = new StringBuilder();
    sb.append(getProject().getBuild().getFinalName());
    if (StringUtils.isNotBlank(classifier)) {
      sb.append("-").append(classifier);
    }
    sb.append(".").append(FILE_EXTENSION_CONFIGURATION);
    return sb.toString();
  }

  private void validateClassifier(String classifier) throws MojoExecutionException {
    // classifier should not contain dots to make sure separation from extension/packaging types is not affected
    if (StringUtils.contains(classifier, ".")) {
      throw new MojoExecutionException("Classifier must not contain dots: " + classifier);
    }
  }

}