org.openl.rules.maven.PackageMojo Maven / Gradle / Ivy
package org.openl.rules.maven;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Paths;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.ArtifactUtils;
import org.apache.maven.model.Dependency;
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.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProjectHelper;
import org.codehaus.plexus.util.DirectoryScanner;
import org.openl.info.OpenLVersion;
import org.openl.rules.dataformat.yaml.YamlMapperFactory;
import org.openl.util.CollectionUtils;
import org.openl.util.FileUtils;
import org.openl.util.ProjectPackager;
import org.openl.util.StringUtils;
import org.openl.util.ZipArchiver;
import org.openl.util.ZipUtils;
/**
* Packages an OpenL Tablets project in a ZIP archive.
*
* @author Yury Molchan
* @since 5.19.1
*/
@Mojo(name = "package", defaultPhase = LifecyclePhase.PACKAGE, threadSafe = true,
requiresDependencyResolution = ResolutionScope.RUNTIME,
requiresDependencyCollection = ResolutionScope.RUNTIME)
public final class PackageMojo extends BaseOpenLMojo {
private static final String DEPLOYMENT_YAML = "deployment.yaml";
static final String DEPLOYMENT_CLASSIFIER = "deployment";
private static final String OPENL_ARTIFACT_TYPE = "zip";
@Parameter(defaultValue = "${project.packaging}", readonly = true)
private String packaging;
@Component
private MavenProjectHelper projectHelper;
/**
* Directory containing the generated artifact.
*/
@Parameter(defaultValue = "${project.build.directory}", required = true)
private File outputDirectory;
@Parameter(defaultValue = "${project.build.finalName}", readonly = true)
private String finalName;
@Parameter(defaultValue = "${project.build.outputDirectory}", required = true, readonly = true)
private File classesDirectory;
/**
* Comma separated list of packaging formats. Supported values: zip, jar.
*/
@Parameter(defaultValue = "zip")
private String format;
/**
* Folder to store dependencies inside the OpenL Tablets project.
*/
@Parameter(defaultValue = "lib/")
private String classpathFolder;
/**
* Classifier that identifies the generated artifact as a supplemental one. By default, if a classifier is not
* provided, the system creates the artifact as the main one. Maven does not support using multiple main artifacts.
* Upon the second attempt to create the main artifact without using a classifier, the build fails.
*/
@Parameter
private String classifier;
/**
* Allowed quantity of dependencies which can be included into the ZIP archive. Usually OpenL Tablets rules require
* a few dependencies, such as domain models, that is, Java beans, or some utils, for example, JSON parsing.
* Typically, the quantity of required dependencies does not exceed 3. If transitive dependencies are declared
* incorrectly, the size of the ZIP package increases dramatically. This parameter allows preventing such situation
* by failing packaging.
*/
@Parameter(defaultValue = "3", required = true)
private int dependenciesThreshold;
/**
* Parameter that enables deployed zip generation. This zip includes an exploded main OpenL Tablets project and all
* dependent OpenL Tablets projects located in separated folders inside the archive.
*/
@Parameter(defaultValue = "false")
private boolean deploymentPackage;
/**
* Deployment archive name.
*/
@Parameter(defaultValue = "${project.build.finalName}")
private String deploymentName;
/**
* Parameter that adds default manifest entries into MANIFEST.MF file.
*
* @since 5.23.4
*/
@Parameter(defaultValue = "true")
private boolean addDefaultManifest;
/**
* Set of key/values to be included to MANIFEST.MF. This parameter overrides default values added by
* {@linkplain #addDefaultManifest} parameter.
*
* @since 5.23.4
*/
@Parameter
private Map manifestEntries;
@Parameter(defaultValue = "${user.name}", readonly = true, required = true)
private String userName;
/**
* Sets the list of include patterns to use. All '/' and '\' characters are replaced by
* File.separatorChar
, so the separator used need not match File.separatorChar
.
*
* When a pattern ends with a '/' or '\', "**" is appended.
*
* If it is not defined, then all files will be included.
*
* @since 5.23.6
*/
@Parameter
private String[] includes;
/**
* Sets the list of exclude patterns to use. All '/' and '\' characters are replaced by
* File.separatorChar
, so the separator used need not match File.separatorChar
.
*
* When a pattern ends with a '/' or '\', "**" is appended.
*
* If it is not defined, then no files will be excluded.
*
* Note: 'pom.xml' file and 'target' directory are excluded always independently on this parameter.
*
* @since 5.23.6
*/
@Parameter
private final String[] excludes = StringUtils.EMPTY_STRING_ARRAY;
@Parameter(defaultValue = "${basedir}", readonly = true, required = true)
private String projectBaseDir;
@Override
void execute(String sourcePath, boolean hasDependencies) throws Exception {
File openLSourceDir = new File(sourcePath);
if (CollectionUtils.isEmpty(openLSourceDir.list())) {
info("No OpenL sources have been found at '", sourcePath, "' path");
info("Skipping packaging of the empty OpenL project.");
return;
}
String[] types = getFormats();
if (CollectionUtils.isEmpty(types)) {
throw new MojoFailureException("No formats have been defined in the plugin configuration.");
}
File dependencyLib = project.getArtifact().getFile();
boolean mainArtifactExists = dependencyLib != null && dependencyLib.isFile();
if (mainArtifactExists && StringUtils.isBlank(classifier) && Arrays.asList(types).contains(packaging)) {
error("The main artifact have been attached already.");
error(
"You have to use classifier to attach supplemental artifacts " +
"to the project instead of replacing them."
);
throw new MojoFailureException("It is not possible to replace the main artifact.");
}
Set dependencies = getDependencies();
int dependenciesSize = dependencies.size();
if (dependenciesSize > dependenciesThreshold) {
error("The quantity of dependencies (",
dependenciesSize,
") exceeds the defined threshold in 'dependenciesThreshold=",
dependenciesThreshold,
"' parameter.");
for (Artifact artifact : dependencies) {
error(" : ", artifact);
}
throw new MojoFailureException("The quantity of dependencies exceeds the limit");
}
final boolean openLJarPackaging = packaging.equals("openl-jar");
if (!mainArtifactExists && CollectionUtils.isNotEmpty(classesDirectory.list()) && !openLJarPackaging) {
// create a jar file with compiled Java sources for OpenL rules
dependencyLib = File.createTempFile(finalName, "-lib.jar", outputDirectory);
JarArchiver.archive(classesDirectory, dependencyLib);
}
DirectoryScanner dirScan = new DirectoryScanner();
dirScan.setBasedir(openLSourceDir);
dirScan.setExcludes(getExcludes());
dirScan.setIncludes(includes);
dirScan.scan();
final String[] includedFiles = dirScan.getIncludedFiles();
for (String type : types) {
File outputFile = getOutputFile(outputDirectory, finalName, classifier, type);
final boolean itselfLink = outputFile.equals(dependencyLib);
try (ZipArchiver arch = new ZipArchiver(outputFile.toPath())) {
if (addDefaultManifest || manifestEntries != null) {
Manifest manifest = createManifest();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
manifest.write(baos);
arch.addFile(new ByteArrayInputStream(baos.toByteArray()), JarFile.MANIFEST_NAME);
}
if (openLJarPackaging && CollectionUtils.isNotEmpty(classesDirectory.list())) {
ProjectPackager.addOpenLProject(classesDirectory, arch);
}
ProjectPackager.addOpenLProject(openLSourceDir, includedFiles, arch);
if (dependencyLib != null && dependencyLib.isFile() && !itselfLink) {
arch.addFile(dependencyLib, classpathFolder + finalName + ".jar");
}
for (Artifact artifact : dependencies) {
File file = artifact.getFile();
arch.addFile(file, classpathFolder + file.getName());
}
}
if (mainArtifactExists || StringUtils.isNotBlank(classifier)) {
info("Attaching the supplemental artifact '", outputFile, ",");
projectHelper.attachArtifact(project, type, classifier, outputFile);
} else {
info("Registering the main artifact '", outputFile, ",");
mainArtifactExists = true;
project.getArtifact().setFile(outputFile);
}
}
if (deploymentPackage && (openLJarPackaging || "openl".equals(packaging))) {
File outputDeploymentDir = new File(outputDirectory, finalName + "-" + DEPLOYMENT_CLASSIFIER);
if (outputDeploymentDir.isDirectory()) {
info("Cleaning up '", outputDeploymentDir, "' directory...");
FileUtils.delete(outputDeploymentDir);
}
outputDeploymentDir.mkdir();
Set openLDependencies = getDependentOpenLProjects();
for (Artifact openLArtifact : openLDependencies) {
if (isRuntimeScope(openLArtifact.getScope())) {
debug("ADD : ", openLArtifact);
File artifactFile = openLArtifact.getFile();
unpackZip(outputDeploymentDir, openLArtifact.getArtifactId(), artifactFile);
}
}
unpackZip(outputDeploymentDir, project.getArtifact().getArtifactId(), project.getArtifact().getFile());
generateDeploymentFile(outputDeploymentDir);
final String artifactType = getFormats()[0];
File outputFile = getOutputFile(outputDirectory,
deploymentName,
DEPLOYMENT_CLASSIFIER,
artifactType);
ZipUtils.archive(outputDeploymentDir, outputFile);
info("Attaching the deployment artifact '", outputFile, ",");
projectHelper.attachArtifact(project, artifactType, DEPLOYMENT_CLASSIFIER, outputFile);
}
}
private String[] getFormats() {
switch (packaging) {
case "openl":
return new String[]{"zip"};
case "openl-jar":
return new String[]{"jar"};
default:
return StringUtils.split(format, ',');
}
}
private Set getDependencies() {
HashSet allowed = getAllowedDependencies();
Set dependencies = new HashSet<>();
for (Artifact artifact : getDependentNonOpenLProjects()) {
String groupId = artifact.getGroupId();
String type = artifact.getType();
String scope = artifact.getScope();
if (skipToProcess(groupId, type, scope)) {
debug("SKIP : ", artifact);
continue;
}
List dependencyTrail = artifact.getDependencyTrail();
if (dependencyTrail.size() < 2) {
debug("SKIP : ", artifact, " (by dependency depth)");
continue; // skip, unexpected size of dependencies
}
if (skipOpenLCoreDependency(dependencyTrail)) {
debug("SKIP : ", artifact, " (transitive dependency from OpenL or SLF4j dependencies)");
continue;
}
String tr = dependencyTrail.get(1);
String key = tr.substring(0, tr.indexOf(':', tr.indexOf(':') + 1));
if (allowed.contains(key)) {
debug("ADD : ", artifact);
dependencies.add(artifact);
}
}
return dependencies;
}
private HashSet getAllowedDependencies() {
HashSet allowed = new HashSet<>();
for (Dependency dep : project.getDependencies()) {
String groupId = dep.getGroupId();
String artifactId = dep.getArtifactId();
String type = dep.getType();
String scope = dep.getScope();
if (skipToProcess(groupId, type, scope)) {
debug("SKIP : ", dep);
} else {
allowed.add(ArtifactUtils.versionlessKey(groupId, artifactId));
}
}
return allowed;
}
private boolean skipToProcess(String groupId, String type, String scope) {
return !isRuntimeScope(scope) || isOpenLCoreDependency(groupId);
}
private boolean isRuntimeScope(String scope) {
return Artifact.SCOPE_RUNTIME.equals(scope) || Artifact.SCOPE_COMPILE.equals(scope);
}
@Override
String getHeader() {
return "OPENL PACKAGING";
}
/**
* Returns the Jar file to generate, based on an optional classifier.
*
* @param basedir the output directory
* @param resultFinalName the name of the ear file
* @param classifier an optional classifier
* @return the file to generate
*/
private File getOutputFile(File basedir, String resultFinalName, String classifier, String format) {
Objects.requireNonNull(basedir, "basedir is not allowed to be null.");
Objects.requireNonNull(resultFinalName, "finalName is not allowed to be null.");
StringBuilder fileName = new StringBuilder(resultFinalName);
if (StringUtils.isNotBlank(classifier)) {
fileName.append('-').append(classifier);
}
fileName.append('.').append(format);
return new File(basedir, fileName.toString());
}
private void unpackZip(File baseDir, String name, File zip) throws IOException {
File outDir = new File(baseDir, name);
ZipUtils.extractAll(zip, outDir);
}
private void generateDeploymentFile(File baseDir) throws IOException {
Map properties = new HashMap<>();
properties.put("name", deploymentName);
try (FileWriter writer = new FileWriter(new File(baseDir, DEPLOYMENT_YAML))) {
YamlMapperFactory.getYamlMapper().writeValue(writer, properties);
}
}
private Manifest createManifest() {
Manifest manifest = new Manifest();
Attributes attributes = manifest.getMainAttributes();
attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
if (addDefaultManifest) {
// initialize with default values
attributes.putValue("Build-Date", ZonedDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
attributes.putValue("Built-By", userName);
attributes.put(Attributes.Name.IMPLEMENTATION_TITLE,
String.format("%s:%s", project.getGroupId(), project.getArtifactId()));
attributes.put(Attributes.Name.IMPLEMENTATION_VERSION, project.getVersion());
if (project.getOrganization() != null) {
attributes.put(Attributes.Name.IMPLEMENTATION_VENDOR, project.getOrganization().getName());
}
attributes.putValue("Created-By", "OpenL Maven Plugin v" + OpenLVersion.getVersion());
}
if (manifestEntries != null) {
for (Map.Entry entry : manifestEntries.entrySet()) {
String key = entry.getKey();
// if value is empty, create an entry with empty string to prevent nulls in file
String value = StringUtils.trimToEmpty(entry.getValue());
attributes.putValue(key, value);
}
}
return manifest;
}
private String[] getExcludes() {
ArrayList strings = new ArrayList<>(excludes.length + 2);
Collections.addAll(strings, excludes);
final String targetDir = Paths.get(projectBaseDir).relativize(outputDirectory.toPath()) + "/**";
strings.add(targetDir);
strings.add("pom.xml");
return strings.toArray(StringUtils.EMPTY_STRING_ARRAY);
}
}