blob: 0d3365a69ec969e23b62a269600824559c967d96 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.tomee.patch.plugin;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
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.MavenProject;
import org.apache.maven.project.artifact.AttachedArtifact;
import org.apache.maven.toolchain.Toolchain;
import org.apache.maven.toolchain.ToolchainManager;
import org.apache.tomee.patch.core.Additions;
import org.apache.tomee.patch.core.Clazz;
import org.apache.tomee.patch.core.Is;
import org.apache.tomee.patch.core.Replacements;
import org.apache.tomee.patch.core.Transformation;
import org.apache.tomee.patch.core.ZipToTar;
import org.codehaus.plexus.compiler.Compiler;
import org.codehaus.plexus.compiler.CompilerConfiguration;
import org.codehaus.plexus.compiler.CompilerMessage;
import org.codehaus.plexus.compiler.CompilerResult;
import org.codehaus.plexus.compiler.manager.CompilerManager;
import org.codehaus.plexus.compiler.manager.NoSuchCompilerException;
import org.tomitribe.jkta.usage.Dir;
import org.tomitribe.jkta.util.Paths;
import org.tomitribe.swizzle.stream.StreamBuilder;
import org.tomitribe.util.Files;
import org.tomitribe.util.IO;
import org.tomitribe.util.Zips;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Mojo(name = "run", requiresDependencyResolution = ResolutionScope.RUNTIME_PLUS_SYSTEM, defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true, threadSafe = true)
public class PatchMojo extends AbstractMojo {
@Parameter(defaultValue = "${project}", readonly = true, required = true)
private MavenProject project;
@Parameter(defaultValue = "${session}", readonly = true, required = true)
private MavenSession session;
@Parameter(defaultValue = "${project.basedir}/src/patch/java", required = true)
private List<File> patchSources;
@Parameter(defaultValue = "${project.basedir}/src/patch/resources", required = true)
private List<File> patchResources;
@Parameter
private List<String> sourceExcludes = new ArrayList<>();
/**
* Regex to identify which archives should be matched
*/
@Parameter(defaultValue = "jakartaee9.*\\.zip", required = true)
private String select;
/**
* <p>
* Specify the requirements for this jdk toolchain.
* This overrules the toolchain selected by the maven-toolchain-plugin.
* </p>
* <strong>note:</strong> requires at least Maven 3.3.1
*
* @since 3.6
*/
@Parameter
private Map<String, String> jdkToolchain;
@Parameter
private Replacements replace;
@Parameter
private Additions add;
@Parameter(defaultValue = "false")
private Boolean createTarGz;
@Parameter(defaultValue = "false")
private Boolean skipTransform;
@Parameter(defaultValue = "false")
private Boolean transformSources;
/**
* Sets the executable of the compiler to use when fork is <code>true</code>.
*/
@Parameter(property = "maven.compiler.executable")
private String executable;
/**
* Version of the compiler to use, ex. "1.3", "1.5", if fork is set to <code>true</code>.
*/
@Parameter(property = "maven.compiler.compilerVersion")
private String compilerVersion;
/**
* The directory to run the compiler from if fork is true.
*/
@Parameter(defaultValue = "${basedir}", required = true, readonly = true)
private File basedir;
/**
* The target directory of the compiler if fork is true.
*/
@Parameter(defaultValue = "${project.build.directory}/patch-classes", required = true, readonly = true)
private File buildDirectory;
/**
* The directory where we will extract the zips being patched so we can compile the
* patch source against the jars contained within.
*/
@Parameter(defaultValue = "${project.build.directory}/patch-classpath", required = true)
private File patchClasspathDirectory;
@Parameter(defaultValue = "${project.build.directory}/patch-sources", required = true)
private File patchSourceDirectory;
@Parameter(defaultValue = "${project.build.directory}/patch-resources", required = true)
private File patchResourceDirectory;
/**
* The -encoding argument for the Java compiler.
*
* @since 2.1
*/
@Parameter(property = "encoding", defaultValue = "${project.build.sourceEncoding}")
private String encoding;
/**
* Plexus compiler manager.
*/
@Component
private CompilerManager compilerManager;
@Component
private ToolchainManager toolchainManager;
/**
* <p>The -source argument for the Java compiler.</p>
*/
@Parameter(property = "maven.compiler.source", defaultValue = "1.8")
protected String source;
/**
* <p>The -target argument for the Java compiler.</p>
*/
@Parameter(property = "maven.compiler.target", defaultValue = "1.8")
protected String target;
/**
* The compiler id of the compiler to use. See this
* <a href="non-javac-compilers.html">guide</a> for more information.
*/
@Parameter(property = "maven.compiler.compilerId", defaultValue = "javac")
private String compilerId;
/**
* Main execution point of the plugin. This looks at the attached artifacts, and runs the transformer on them.
*
* @throws MojoExecutionException Thrown if there is an error during plugin execution
*/
public void execute() throws MojoExecutionException, CompilationFailureException {
try {
Files.mkdir(patchClasspathDirectory);
// Select the zip files and jars we'll be potentially patching
final List<Artifact> artifacts = getPatchArtifacts();
prepareResources();
// Extract any zips and return a list of jars
final List<File> jars = prepareJars(artifacts);
compile(jars);
final List<Clazz> clazzes = classes();
final Transformation transformation = new Transformation(clazzes, patchResourceDirectory, replace, add, new MavenLog(getLog()), skipTransform);
for (final Artifact artifact : artifacts) {
final File file = artifact.getFile();
getLog().debug("Patching " + file.getAbsolutePath());
final File patched = transformation.transformArchive(file);
IO.copy(patched, file);
if (createTarGz && file.getName().endsWith(".zip")) {
final File tarGz;
try {
tarGz = ZipToTar.toTarGz(file);
} catch (Exception e) {
getLog().error("Failed to create tar.gz from " + file.getAbsolutePath(), e);
continue;
}
final String classifier = artifact.getClassifier();
final AttachedArtifact attachedArtifact = new AttachedArtifact(project.getArtifact(), "tar.gz", classifier, project.getArtifact().getArtifactHandler());
attachedArtifact.setFile(tarGz);
attachedArtifact.setResolved(true);
project.addAttachedArtifact(attachedArtifact);
}
}
transformation.complete();
} catch (IOException e) {
throw new MojoExecutionException("Error occurred during execution", e);
}
}
private void prepareResources() throws MojoExecutionException {
final File defaultPatchResources = new File(basedir, "src/patch/resources");
Files.mkdir(patchResourceDirectory);
for (final File patchResource : patchResources) {
if (patchResource == null) continue;
if (!patchResource.exists()) {
if (patchResource.getAbsolutePath().equals(defaultPatchResources.getAbsolutePath())) {
// If the default directory does not exist, the user likely did not explicitly
// ask for it. Just silently skip it.
continue;
}
final String message = "Patch resource directory does not exist: " + patchResource.getAbsolutePath();
getLog().error(message);
throw new MojoExecutionException(message);
}
if (!patchResource.isDirectory()) {
final String message = "Patch resource directory is not a directory: " + patchResource.getAbsolutePath();
getLog().error(message);
throw new MojoExecutionException(message);
}
}
patchResources.stream()
.filter(Objects::nonNull)
.filter(File::exists)
.forEach(file -> copy(file, file, patchResourceDirectory));
}
private List<Clazz> classes() {
return Dir.from(buildDirectory).files()
.filter(file -> file.getName().endsWith(".class"))
.map(file -> new Clazz(Paths.childPath(buildDirectory, file), file))
.collect(Collectors.toList());
}
/**
* Any zip files contained in the Artifact set should be extracted
* Any jar files contained in the Artifact set will be returned as-is
*/
private List<File> prepareJars(final List<Artifact> artifacts) throws IOException {
// Extract all zip, war, ear, rar files. Do not extract jar files.
for (final Artifact artifact : artifacts) {
if (isZip(artifact.getFile()) && !isJar(artifact.getFile())) {
Zips.unzip(artifact.getFile(), patchClasspathDirectory);
}
}
// Collect a list of jars
final List<File> jars = new ArrayList<>();
// Add any artifacts that are already jars
artifacts.stream()
.map(Artifact::getFile)
.filter(File::isFile)
.filter(this::isJar)
.forEach(jars::add);
// Add any extracted files that are jars
Dir.from(patchClasspathDirectory)
.files()
.filter(File::isFile)
.filter(this::isJar)
.forEach(jars::add);
return jars;
}
private boolean isJar(final File file) {
return file.getName().endsWith(".jar");
}
private static boolean isZip(final File file) {
return new Is.Zip().accept(file);
}
private List<Artifact> getPatchArtifacts() {
final Predicate<String> match = Pattern.compile(select).asPredicate();
final Artifact[] available = getSourceArtifacts();
final List<Artifact> selected = Stream.of(available)
.filter(artifact -> match.test(artifact.getFile().getName()))
.collect(Collectors.toList());
if (selected.size() == 0) {
final String message = String.format("No artifacts matched expression '%s'. %s available artifacts:", select, available.length);
getLog().error(message);
Arrays.stream(available)
.map(artifact -> artifact.getFile().getName())
.forEach(s -> getLog().error(" - " + s));
throw new NoMatchingArtifactsException(select);
}
return selected;
}
private void compile(final List<File> jars) throws MojoExecutionException, CompilationFailureException {
getLog().debug("Using compiler '" + compilerId + "'.");
final Compiler compiler;
try {
compiler = compilerManager.getCompiler(compilerId);
} catch (NoSuchCompilerException e) {
throw new MojoExecutionException("No such compiler '" + e.getCompilerId() + "'.");
}
final Toolchain tc = getToolchain();
if (tc != null) {
getLog().info("Toolchain in maven-compiler-plugin: " + tc);
//TODO somehow shaky dependency between compilerId and tool executable.
executable = tc.findTool(compilerId);
}
final File defaultPatchSources = new File(basedir, "src/patch/java");
Files.mkdir(patchSourceDirectory);
for (final File patchSource : patchSources) {
if (patchSource == null) continue;
if (!patchSource.exists()) {
if (patchSource.getAbsolutePath().equals(defaultPatchSources.getAbsolutePath())) {
// If the default directory does not exist, the user likely did not explicitly
// ask for it. Just silently skip it.
continue;
}
final String message = "Patch source directory does not exist: " + patchSource.getAbsolutePath();
getLog().error(message);
throw new MojoExecutionException(message);
}
if (!patchSource.isDirectory()) {
final String message = "Patch source directory is not a directory: " + patchSource.getAbsolutePath();
getLog().error(message);
throw new MojoExecutionException(message);
}
}
patchSources.stream()
.filter(Objects::nonNull)
.filter(File::exists)
.forEach(file -> copy(file, file, patchSourceDirectory));
final CompilerConfiguration compilerConfiguration = new CompilerConfiguration();
compilerConfiguration.setOutputLocation(buildDirectory.getAbsolutePath());
compilerConfiguration.setOptimize(false);
compilerConfiguration.setDebug(true);
compilerConfiguration.setParameters(false);
compilerConfiguration.setVerbose(false);
compilerConfiguration.setShowWarnings(false);
compilerConfiguration.setFailOnWarning(false);
compilerConfiguration.setShowDeprecation(false);
compilerConfiguration.setSourceVersion(source);
compilerConfiguration.setTargetVersion(target);
compilerConfiguration.setReleaseVersion(null);
compilerConfiguration.setProc(null);
compilerConfiguration.setSourceLocations(Collections.singletonList(patchSourceDirectory.getAbsolutePath()));
compilerConfiguration.setAnnotationProcessors(null);
compilerConfiguration.setSourceEncoding(encoding);
compilerConfiguration.setFork(true);
compilerConfiguration.setExecutable(executable);
compilerConfiguration.setWorkingDirectory(basedir);
compilerConfiguration.setCompilerVersion(compilerVersion);
compilerConfiguration.setBuildDirectory(buildDirectory);
compilerConfiguration.setOutputFileName(null);
// Add each jar as a classpath entry
jars.stream()
.map(File::getAbsolutePath)
.forEach(compilerConfiguration::addClasspathEntry);
// Now we can compile!
final CompilerResult compilerResult;
try {
compilerResult = compiler.performCompile(compilerConfiguration);
} catch (Exception e) {
throw new MojoExecutionException("Fatal error compiling", e);
}
List<CompilerMessage> warnings = new ArrayList<>();
List<CompilerMessage> errors = new ArrayList<>();
List<CompilerMessage> others = new ArrayList<>();
for (CompilerMessage message : compilerResult.getCompilerMessages()) {
if (message.getKind() == CompilerMessage.Kind.ERROR) {
errors.add(message);
} else if (message.getKind() == CompilerMessage.Kind.WARNING
|| message.getKind() == CompilerMessage.Kind.MANDATORY_WARNING) {
warnings.add(message);
} else {
others.add(message);
}
}
if (!compilerResult.isSuccess()) {
for (CompilerMessage message : others) {
assert message.getKind() != CompilerMessage.Kind.ERROR
&& message.getKind() != CompilerMessage.Kind.WARNING
&& message.getKind() != CompilerMessage.Kind.MANDATORY_WARNING;
getLog().info(message.toString());
}
if (!warnings.isEmpty()) {
getLog().info("-------------------------------------------------------------");
getLog().warn("COMPILATION WARNING : ");
getLog().info("-------------------------------------------------------------");
for (CompilerMessage warning : warnings) {
getLog().warn(warning.toString());
}
getLog().info(warnings.size() + ((warnings.size() > 1) ? " warnings " : " warning"));
getLog().info("-------------------------------------------------------------");
}
if (!errors.isEmpty()) {
getLog().info("-------------------------------------------------------------");
getLog().error("COMPILATION ERROR : ");
getLog().info("-------------------------------------------------------------");
for (CompilerMessage error : errors) {
getLog().error(error.toString());
}
getLog().info(errors.size() + ((errors.size() > 1) ? " errors " : " error"));
getLog().info("-------------------------------------------------------------");
}
if (!errors.isEmpty()) {
throw new CompilationFailureException(errors);
} else {
throw new CompilationFailureException(warnings);
}
} else {
for (CompilerMessage message : compilerResult.getCompilerMessages()) {
switch (message.getKind()) {
case NOTE:
case OTHER:
getLog().info(message.toString());
break;
case ERROR:
getLog().error(message.toString());
break;
case MANDATORY_WARNING:
case WARNING:
default:
getLog().warn(message.toString());
break;
}
}
}
}
private void copy(final File root, final File src, final File dest) {
copy:
for (final File file : src.listFiles()) {
final String path = file.getAbsolutePath().substring(root.getAbsolutePath().length() + 1);
for (final String exclude : sourceExcludes) {
if (path.matches(exclude)) {
getLog().debug("Exclude source file: " + file.getAbsolutePath());
continue copy;
}
}
if (file.isDirectory()) {
final File dir = new File(dest, file.getName());
Files.mkdir(dir);
copy(root, file, dir);
} else if (file.isFile()) {
try (InputStream in = IO.read(file)) {
if (!skipTransform && transformSources) {
IO.copy(updateImports(in), new File(dest, file.getName()));
} else {
IO.copy(in, new File(dest, file.getName()));
}
} catch (IOException e) {
throw new UncheckedIOException("Cannot copy file " + file.getAbsolutePath(), e);
}
}
}
}
private InputStream updateImports(final InputStream in) {
return StreamBuilder.create(in)
.replace("javax.activation", "jakarta.activation")
.replace("javax.annotation", "jakarta.annotation")
.replace("javax.batch", "jakarta.batch")
.replace("javax.decorator", "jakarta.decorator")
.replace("javax.ejb", "jakarta.ejb")
.replace("javax.el", "jakarta.el")
.replace("javax.enterprise", "jakarta.enterprise")
.replace("javax.inject", "jakarta.inject")
.replace("javax.interceptor", "jakarta.interceptor")
.replace("javax.jms", "jakarta.jms")
.replace("javax.json", "jakarta.json")
.replace("javax.json.bind", "jakarta.json.bind")
.replace("javax.jws", "jakarta.jws")
.replace("javax.mail", "jakarta.mail")
.replace("javax.persistence", "jakarta.persistence")
.replace("javax.resource", "jakarta.resource")
.replace("javax.security.auth.message", "jakarta.security.auth.message")
.replace("javax.security.enterprise", "jakarta.security.enterprise")
.replace("javax.security.jacc", "jakarta.security.jacc")
.replace("javax.servlet", "jakarta.servlet")
.replace("javax.transaction", "jakarta.transaction")
.replace("javax.validation", "jakarta.validation")
.replace("javax.websocket", "jakarta.websocket")
.replace("javax.ws.rs", "jakarta.ws.rs")
.replace("javax.xml.bind", "jakarta.xml.bind")
.replace("javax.xml.soap", "jakarta.xml.soap")
.replace("javax.xml.ws", "jakarta.xml.ws")
// These sub packages to the above must be renamed back
.replace("jakarta.annotation.process", "javax.annotation.process")
.replace("jakarta.enterprise.deploy", "javax.enterprise.deploy")
.replace("jakarta.transaction.xa", "javax.transaction.xa")
.get()
;
}
//TODO remove the part with ToolchainManager lookup once we depend on
//3.0.9 (have it as prerequisite). Define as regular component field then.
protected final Toolchain getToolchain() {
Toolchain tc = null;
if (jdkToolchain != null) {
// Maven 3.3.1 has plugin execution scoped Toolchain Support
try {
Method getToolchainsMethod =
toolchainManager.getClass().getMethod("getToolchains", MavenSession.class, String.class,
Map.class);
@SuppressWarnings("unchecked")
List<Toolchain> tcs =
(List<Toolchain>) getToolchainsMethod.invoke(toolchainManager, session, "jdk",
jdkToolchain);
if (tcs != null && !tcs.isEmpty()) {
tc = tcs.get(0);
}
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException e) {
// ignore
}
}
if (tc == null) {
tc = toolchainManager.getToolchainFromBuildContext("jdk", session);
}
return tc;
}
/**
* Gets the source artifacts that should be transformed
*
* @return an array to artifacts to be transformed
*/
public Artifact[] getSourceArtifacts() {
List<Artifact> artifactList = new ArrayList<Artifact>();
if (project.getArtifact() != null && project.getArtifact().getFile() != null) {
artifactList.add(project.getArtifact());
}
for (final Artifact attachedArtifact : project.getAttachedArtifacts()) {
if (attachedArtifact.getFile() != null) {
artifactList.add(attachedArtifact);
}
}
return artifactList.toArray(new Artifact[0]);
}
private static class NoMatchingArtifactsException extends RuntimeException {
public NoMatchingArtifactsException(final String select) {
super(String.format("No artifacts matched expression '%s'", select));
}
}
}