blob: 340752215fa9213915e069b30c6bd37ed88fa08e [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.bootstrap;
import lombok.Data;
import lombok.Getter;
import lombok.ToString;
import org.apache.openejb.loader.Files;
import org.apache.openejb.loader.IO;
import org.apache.openejb.loader.JarLocation;
import org.apache.openejb.loader.Zips;
import org.apache.openejb.util.Join;
import org.apache.openejb.util.Strings;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UncheckedIOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* The BOM files in the boms/tomee-microprofile/ and directories are
* generated by this class.
*
* The goal of each BOM is to be completely identical to the respective
* standalone server zip. Each library from the zip is translated back
* to its respective maven coordinate and placed in the pom with all
* transitive dependencies removed. Thus the pom is a 1-to-1 mapping
* of the server zip itself with zero variance possible due to any
* transitive Maven dependencies.
*
* Currently, the generation process is manual and should ideally be
* done prior to release.
*
* @see GenerateBoms#main(String[]) to execute this generator
*/
public class GenerateBoms {
private final File project;
private final File boms;
private final File dists;
private final Repository repository;
public GenerateBoms() {
/*
* Resolve all project paths relative to this built class
* that lives in `tomee/tomee-bootstrap/target/test-classes/`
*
* We walk backwards from that directory till we find the
* project root and then we can build all relative paths
* from there.
*/
final File testClasses = JarLocation.jarLocation(GenerateBoms.class);
final File target = testClasses.getParentFile();
final File tomeeBootstrap = target.getParentFile();
final File tomee = tomeeBootstrap.getParentFile();
this.project = tomee.getParentFile();
this.boms = new File(project, "boms");
this.dists = new File(tomee, "apache-tomee/target");
{ // Find the ~/.m2/repository directory
final File junitJar = JarLocation.jarLocation(Test.class);
final File version = junitJar.getParentFile();
final File artifact = version.getParentFile();
final File group = artifact.getParentFile();
final File repository = group.getParentFile();
this.repository = new Repository(repository);
}
Files.dir(project);
Files.dir(boms);
Files.dir(dists);
}
/**
* Use this main method from your IDE to regenerate the TomEE BOM files.
*/
public static void main(String[] args) throws Exception {
new GenerateBoms().run();
}
/**
* Navigate to `tomee/apache-tomee/target/` and collect any apache-tomee*.zip files.
* Translate each into a Distribution instance that contains the complete list of
*
* @see GenerateBoms#asDistribution(File)
*/
public void run() {
if (!dists.exists()) {
throw new IllegalStateException("Directory does not exist: " + dists.getAbsolutePath() + "\n" +
"Ensure the project has been built and the server zips exist prior to executing" +
" this generator.");
}
final List<Distribution> distributions = Stream.of(dists.listFiles())
.filter(file -> file.getName().endsWith(".zip"))
.filter(file -> file.getName().startsWith("apache-tomee-"))
.map(this::asDistribution)
.collect(Collectors.toList());
verify(distributions);
distributions.forEach(this::toBom);
}
/**
* If there are files inside a zip that we could not map
* back to an artifact in the local Maven repo, then the
* corresponding BOM will be incomplete and we must throw
* an error.
*
* People will not check warnings or log output of any kind
* so we must be obnixious with this.
*/
private void verify(final List<Distribution> distributions) {
final List<Distribution> incomplete = distributions.stream()
.filter(distribution -> distribution.getMissing().size() > 0)
.collect(Collectors.toList());
if (incomplete.size() > 0) {
throw new IncompleteMappingException(incomplete);
}
}
/**
* Overwrite (or create) the contents of the BOM files in:
*
* - $project.dir/boms/tomee-microprofile
* - $project.dir/boms/tomee-webprofile
* - $project.dir/boms/tomee-plume
* - $project.dir/boms/tomee-plus
*/
private void toBom(final Distribution distribution) {
try {
final URL url = this.getClass().getClassLoader().getResource("pom-template.xml");
final String template = IO.slurp(url);
final String dependencies = Join.join("", Artifact::asBomDep, distribution.getArtifacts());
final String pom = template.replace("TomEE MicroProfile", distribution.getDisplayName())
.replace("tomee-microprofile", distribution.getName())
.replace("<!--dependencies-->", dependencies);
final File dist = Files.mkdir(boms, distribution.getName());
final File pomFile = new File(dist, "pom.xml");
IO.copy(IO.read(pom), pomFile);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* Represents a particular apache-tomee-foo-1.2.3.zip
*/
@Data
public static class Distribution {
/**
* The Maven coordinates for any jars found inside
* either tomee.home/lib/* or tomee.home/bin/*
*/
private final List<Artifact> artifacts = new ArrayList<>();
/**
* The names of any files in tomee.home/lib/* or
* tomee.home/bin/* that could not be matched with
* something from the local Maven repository
*/
private final List<File> missing = new ArrayList<>();
/**
* The corresponding apache-tomee-foo-1.2.3.zip
*/
private final File zip;
/**
* The short name of the distribution. For example
* `tomee-webprofile` for apache-tomee-webprofile-1.2.3.zip
*/
private final String name;
/**
* The display name of the distribution. For example
* `TomEE WebProfile` for apache-tomee-webprofile-1.2.3.zip
*/
private final String displayName;
public Distribution(final File zip) {
this.zip = zip;
name = zip.getName()
.replaceFirst("-[0-9].*", "")
.replace("apache-", "");
this.displayName = Stream.of(name.split("-"))
.map(Strings::ucfirst)
.map(s -> s.replace("ee", "EE"))
.map(s -> s.replace("profile", "Profile"))
.reduce((s, s2) -> s + " " + s2)
.get();
}
@Override
public String toString() {
return "Distribution{" +
"displayName=" + displayName +
", name=" + name +
", artifacts=" + artifacts.size() +
", missing=" + missing.size() +
'}';
}
}
/**
* Extract the zip that represents a TomEE distribution. Find
* all jar files. Match them up with jars in the local Maven
* repository. Identify any jars we could not match. And finally,
* return all the data so we have a canonical representation of
* the distribution.
*/
private Distribution asDistribution(final File zip) {
final File tmpdir = Files.tmpdir();
try {
Zips.unzip(zip, tmpdir);
} catch (IOException e) {
throw new UncheckedIOException("Cannot unzip " + zip.getAbsolutePath(), e);
}
final Distribution distribution = new Distribution(zip);
final List<File> jars = Files.collect(tmpdir, ".*.jar");
final Function<File, Artifact> from = file1 -> {
try {
return repository.from(file1);
} catch (IllegalStateException e) {
distribution.missing.add(file1);
return null;
}
};
jars.stream()
.filter(jar -> !jar.getName().equals("bootstrap.jar"))
.filter(jar -> !jar.getName().equals("catalina-ant.jar"))
.filter(jar -> !jar.getName().startsWith("tomcat-i18n"))
.map(from)
.filter(Objects::nonNull)
.sorted()
.forEach(distribution.artifacts::add);
return distribution;
}
/**
* This class represents the local Maven repository and is used to
* try and find the correct Maven coordinates for the jars inside
* the tomee/lib/ and tomee/bin/ directories.
*
* This does not attempt any online resolution to Maven Central itself
* as theoretically you just built all the TomEE dists so the jars that
* ended up in that dist where already downloaded and we just need to
* find them.
*/
public static class Repository {
private final Map<String, File> artifacts = new HashMap<>();
private final File path;
public Repository(final File path) {
this.path = path;
final List<File> jars = Files.collect(this.path, ".*\\.jar");
for (final File jar : jars) {
this.artifacts.put(jar.getName(), jar);
}
}
/**
* In several situations the jars inside the tomee/lib/*.jar or tomee/bin/*.jar
* are a little tricky to map back to something from Maven Central.
*
* The logic in this method handles all the edge cases.
*
* One of the most confusing is that nearly all `catalina-foo.jar` files do
* not contain any version, nor do they use the same artifact name that would
* exist in Maven Central. For Tomcat, all the `catalina-foo.jar` files tend
* to map to `tomcat-foo-1.2.3.jar` files in Maven Central.
*
* There is another known limitation that the Eclipse Compiler jar (ecj-4.12.jar)
* found in the Tomcat distribution is not available in Maven Central. The Tomcat
* build will download it directly from the Eclipse website. Very strangely, the
* Eclipse Compiler team does publish jars to Maven Central, but only for version 3.x
*/
public Artifact from(final File jar) {
if (jar.getName().equals("commons-daemon.jar")) {
return new Artifact("commons-daemon", "commons-daemon", "1.1.0");
}
if (jar.getName().equals("tomcat-juli.jar")) {
return new Artifact("org.apache.tomee", "tomee-juli", "${project.version}");
}
if (jar.getName().equals("catalina-ha.jar")) {
return new Artifact("org.apache.tomcat", "tomcat-catalina-ha", "${tomcat.version}");
}
if (jar.getName().equals("catalina-storeconfig.jar")) {
return new Artifact("org.apache.tomcat", "tomcat-storeconfig", "${tomcat.version}");
}
if (jar.getName().equals("catalina-tribes.jar")) {
return new Artifact("org.apache.tomcat", "tomcat-tribes", "${tomcat.version}");
}
if (jar.getName().equals("catalina-ssi.jar")) {
return new Artifact("org.apache.tomcat", "tomcat-ssi", "${tomcat.version}");
}
if (jar.getName().equals("catalina.jar")) {
return new Artifact("org.apache.tomcat", "tomcat-catalina", "${tomcat.version}");
}
if (jar.getName().equals("el-api.jar")) {
return new Artifact("org.apache.tomcat", "tomcat-el-api", "${tomcat.version}");
}
if (jar.getName().equals("jasper-el.jar")) {
return new Artifact("org.apache.tomcat", "tomcat-jasper-el", "${tomcat.version}");
}
if (jar.getName().equals("jasper.jar")) {
return new Artifact("org.apache.tomcat", "tomcat-jasper", "${tomcat.version}");
}
if (jar.getName().equals("jaspic-api.jar")) {
return new Artifact("org.apache.tomcat", "tomcat-jaspic-api", "${tomcat.version}");
}
if (jar.getName().equals("servlet-api.jar")) {
return new Artifact("org.apache.tomcat", "tomcat-servlet-api", "${tomcat.version}");
}
if (jar.getName().equals("websocket-api.jar")) {
return new Artifact("org.apache.tomcat", "tomcat-websocket-api", "${tomcat.version}");
}
if (jar.getName().equals("tomcat-coyote.jar")) {
return new Artifact("org.apache.tomcat", "tomcat-coyote", "${tomcat.version}");
}
if (jar.getName().equals("tomcat-dbcp.jar")) {
return new Artifact("org.apache.tomcat", "tomcat-dbcp", "${tomcat.version}");
}
if (jar.getName().equals("tomcat-api.jar")) {
return new Artifact("org.apache.tomcat", "tomcat-api", "${tomcat.version}");
}
if (jar.getName().equals("tomcat-websocket.jar")) {
return new Artifact("org.apache.tomcat", "tomcat-websocket", "${tomcat.version}");
}
if (jar.getName().equals("tomcat-util.jar")) {
return new Artifact("org.apache.tomcat", "tomcat-util", "${tomcat.version}");
}
if (jar.getName().equals("tomcat-util-scan.jar")) {
return new Artifact("org.apache.tomcat", "tomcat-util-scan", "${tomcat.version}");
}
if (jar.getName().equals("tomcat-jni.jar")) {
return new Artifact("org.apache.tomcat", "tomcat-jni", "${tomcat.version}");
}
if (jar.getName().equals("tomcat-jdbc.jar")) {
return new Artifact("org.apache.tomcat", "tomcat-jdbc", "${tomcat.version}");
}
if (jar.getName().equals("jsp-api.jar")) {
return new Artifact("org.apache.tomcat", "tomcat-jsp-api", "${tomcat.version}");
}
if (jar.getName().startsWith("ecj-")) {
return new Artifact("org.eclipse.jdt", "ecj", "3.22.0");
}
if (jar.getName().equals("openejb-javaagent.jar")) {
return new Artifact("org.apache.tomee", "openejb-javaagent", "${project.version}");
}
if (jar.getName().startsWith("openejb-") ||
jar.getName().startsWith("tomee-") ||
jar.getName().startsWith("mp-common-") ||
jar.getName().startsWith("mp-jwt-") ||
jar.getName().startsWith("mbean-annotation-")) {
final String artifact = jar.getName().replaceAll("-8.0.*", "");
return new Artifact("org.apache.tomee", artifact, "${project.version}");
}
// /Users/dblevins/.m2/repository//org/apache/xbean/xbean-naming/4.14/xbean-naming-4.14.jar
final File file = getFile(jar);
final File versionDir = file.getParentFile();
final File artifactDir = versionDir.getParentFile();
final String groupId = artifactDir.getParentFile()
.getAbsolutePath()
.substring(path.getAbsolutePath().length() + 1)
.replace("/", ".");
return Artifact.builder()
.artifactId(artifactDir.getName())
.version(versionDir.getName())
.groupId(groupId)
.build();
}
private File getFile(final File jar) {
{
final File file = artifacts.get(jar.getName());
if (file != null) return file;
}
{
final String name = jar.getName();
final String relativeName = name.substring(name.length() - 4);
for (final Map.Entry<String, File> entry : artifacts.entrySet()) {
if (entry.getKey().startsWith(relativeName)) return entry.getValue();
}
}
throw new IllegalStateException(jar.getName());
}
}
/**
* A simple representation of a Maven Coordinate
*/
@Getter
@ToString
@lombok.Builder(toBuilder = true)
public static class Artifact implements Comparable<Artifact> {
private final String groupId;
private final String artifactId;
private final String version;
@Override
public int compareTo(final Artifact o) {
final String a = this.getGroupId() + ":" + this.artifactId;
final String b = o.getGroupId() + ":" + o.artifactId;
return a.compareTo(b);
}
/**
* Long term the dep in the BOM should not have a version
* and all such data would be in the parent pom.
*/
public String asBomDep() {
final String g = groupId;
final String a = artifactId;
final String v = version;
return "" +
" <dependency>\n" +
" <groupId>" + g + "</groupId>\n" +
" <artifactId>" + a + "</artifactId>\n" +
" <version>" + v + "</version>\n" +
" <exclusions>\n" +
" <exclusion>\n" +
" <artifactId>*</artifactId>\n" +
" <groupId>*</groupId>\n" +
" </exclusion>\n" +
" </exclusions>\n" +
" </dependency>\n";
}
/**
* Currently unused, but long term this will be the entry we'd need
* to add to the parent pom to ensure that the BOMs can just list
* the dependencies needed, but not duplicate the version information.
*/
public String asManagedDep() {
final String g = groupId;
final String a = artifactId;
final String v = version;
return "" +
" <dependency>\n" +
" <groupId>" + g + "</groupId>\n" +
" <artifactId>" + a + "</artifactId>\n" +
" <version>" + v + "</version>\n" +
" </dependency>\n";
}
}
/**
* Attempt to throw a detailed error message if we are unable to find the
* matching maven coordinates for a particular lib/*.jar
*/
public static class IncompleteMappingException extends IllegalStateException {
private final List<Distribution> incomplete;
public IncompleteMappingException(final List<Distribution> incomplete) {
super(message(incomplete));
this.incomplete = incomplete;
}
public List<Distribution> getIncomplete() {
return incomplete;
}
private static String message(final List<Distribution> incomplete) {
final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
final PrintStream out = new PrintStream(bytes);
out.println("Unable to find matching maven coordinates from the following distributions.");
for (Distribution distribution : incomplete) {
out.printf(" %s%n", distribution.getZip().getName());
for (File file : distribution.getMissing()) {
out.printf(" - %s%n", file.getName());
}
}
out.flush();
return new String(bytes.toByteArray());
}
}
}