package org.jboss.windup.rules.apps.mavenize; import java.nio.file.Path; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.logging.Logger; import org.apache.commons.collections4.OrderedMap; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; import org.jboss.windup.graph.GraphContext; import org.jboss.windup.graph.model.ArchiveModel; import org.jboss.windup.graph.model.OrganizationModel; import org.jboss.windup.graph.model.ProjectModel; import org.jboss.windup.graph.model.TechnologyReferenceModel; import org.jboss.windup.graph.model.WindupConfigurationModel; import org.jboss.windup.graph.model.resource.FileModel; import org.jboss.windup.rules.apps.java.archives.model.ArchiveCoordinateModel; import org.jboss.windup.rules.apps.java.archives.model.IdentifiedArchiveModel; import org.jboss.windup.rules.apps.java.model.project.MavenProjectModel; import org.jboss.windup.util.Logging; /** * Performs the actions needed to mavenize an application. * * @author <a href="http://ondra.zizka.cz/">Ondrej Zizka, ozizka at seznam.cz</a> */ public class MavenizationService { private static final Logger LOG = Logging.get(MavenizationService.class); public static final String OUTPUT_SUBDIR_MAVENIZED = "mavenized"; private final GraphContext grCtx; MavenizationService(GraphContext graphContext) { this.grCtx = graphContext; } /** * For the given application (Windup input), creates a stub of Mavenized project. * <p> * The resulting structure is: (+--- is "module", +~~~ is a dependency) * <pre> * Parent POM * +--- BOM * +~~~ JBoss EAP BOM * +--- JAR submodule * +~~~ library JARs * +--- WAR * +~~~ library JARs * +~~~ JAR submodule * +--- EAR * +~~~ library JAR * +~~~ JAR submodule * +~~~ WAR submodule * </pre> */ void mavenizeApp(ProjectModel projectModel) { LOG.info("Mavenizing ProjectModel " + projectModel.toPrettyString()); MavenizationContext mavCtx = new MavenizationContext(); mavCtx.graphContext = grCtx; WindupConfigurationModel config = grCtx.getUnique(WindupConfigurationModel.class); mavCtx.mavenizedBaseDir = config.getOutputPath().asFile().toPath().resolve(OUTPUT_SUBDIR_MAVENIZED); mavCtx.unifiedGroupId = new ModuleAnalysisHelper(grCtx).deriveGroupId(projectModel); mavCtx.unifiedAppName = normalizeDirName(projectModel.getName()); mavCtx.unifiedVersion = "1.0"; // 1) create the overall structure - a parent, and a BOM. // Root pom.xml ( serves as a parent pom.xml in our resulting structure). mavCtx.rootPom = new Pom(new MavenCoord(mavCtx.getUnifiedGroupId(), mavCtx.getUnifiedAppName() + "-parent", mavCtx.getUnifiedVersion())); mavCtx.rootPom.role = Pom.ModuleRole.PARENT; mavCtx.rootPom.name = projectModel.getName() + " - Parent"; mavCtx.rootPom.description = "Parent of " + projectModel.getName(); mavCtx.rootPom.root = true; final String bomArtifactId = mavCtx.getUnifiedAppName() + "-bom"; // BOM Pom bom = new Pom(new MavenCoord(mavCtx.getUnifiedGroupId(), bomArtifactId, mavCtx.getUnifiedVersion())); bom.bom = getTargetTechnologies().contains("eap7") ? MavenizeRuleProvider.JBOSS_BOM_JAVAEE7_WITH_ALL : MavenizeRuleProvider.JBOSS_BOM_JAVAEE6_WITH_ALL; bom.role = Pom.ModuleRole.BOM; bom.parent = new Pom(MavenizeRuleProvider.JBOSS_PARENT); bom.description = "Bill of Materials. See https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html"; bom.name = projectModel.getName() + " - BOM"; mavCtx.getRootPom().submodules.put(bomArtifactId, bom); mavCtx.bom = bom; // BOM - dependencyManagement dependencies for( ArchiveCoordinateModel dep : grCtx.getUnique(GlobalBomModel.class).getDependencies() ){ LOG.info("Adding dep to BOM: " + dep.toPrettyString()); bom.dependencies.add(new SimpleDependency(Dependency.Role.LIBRARY, MavenCoord.from(dep))); } // 2) Recursively add the modules. mavCtx.rootAppPom = mavenizeModule(mavCtx, projectModel, null); // TODO: MIGR-236 Sort the modules. ///mavCtx.rootPom.submodules = sortSubmodulesToReflectDependencies(mavCtx.rootAppPom); // 3) Write the pom.xml's. new MavenStructureRenderer(mavCtx).createMavenProjectDirectoryTree(); } /** * Mavenizes the particular project module, i.e. single pom.xml. * Then continues recursively to submodules. * * @return null if the project can't be processed for some reason, e.g. is from an unparsable jar. */ private Pom mavenizeModule(MavenizationContext mavCtx, ProjectModel projectModel, Pom containingModule) { LOG.info("Mavenizing submodule ProjectModel " + projectModel.toPrettyString()); LOG.info(" Root file: " + projectModel.getRootFileModel().toPrettyString()); LOG.info(" Containing module: " + containingModule); if (projectModel.getRootFileModel().getParseError() != null) return null; // Known library -> add as a dependency and skip. // SHA1 identified archive? if (projectModel.getRootFileModel() instanceof IdentifiedArchiveModel) { final IdentifiedArchiveModel idArch = (IdentifiedArchiveModel)projectModel.getRootFileModel(); if (idArch == null) LOG.warning("Project's IdentifiedArchiveModel getRootFileModel() returned null."); else if (idArch.getCoordinate() == null) LOG.warning("Project's IdentifiedArchiveModel getRootFileModel().getCoordinate() returned null."); else if(containingModule == null) LOG.warning("containingModule is null."); // IllegalStateEx? else containingModule.dependencies.add(new SimpleDependency(Dependency.Role.LIBRARY, MavenCoord.from(idArch.getCoordinate()))); LOG.info("Known library, skipping recursive mavenization: " + idArch.getCoordinate()); return null; } // ArchiveModel's organizations contain a known org, like Apache. if (projectModel.getRootFileModel() instanceof ArchiveModel) { Set<String> skipOrganizations = new HashSet(Arrays.asList("Apache Sun Iona IBM Codehaus Spring Sonatype JBoss Oracle".toLowerCase().split(""))); final ArchiveModel arch = (ArchiveModel)projectModel.getRootFileModel(); for (OrganizationModel org : arch.getOrganizationModels()) { if (skipOrganizations.contains(org.getName().toLowerCase())) { LOG.info("Library from 3rd party vendor ("+org.getName()+"), skipping recursive mavenization: " + arch.getFilePath()); return null; } } } // A MavenProject. // TODO: This covers both app modules and libraries with pom.xml inside. Need to diferentiate - skip only libraries. // TODO: This should disappear after WINDUP-981 if (false && projectModel instanceof MavenProjectModel) { final MavenProjectModel mvnProject = (MavenProjectModel)projectModel; if (mvnProject.getMavenIdentifier() == null) LOG.warning("MavenProject's getMavenIdentifier() returned null."); else if(containingModule == null) LOG.warning("containingModule is null."); // IllegalStateEx? else if (true /* isLibraryAndNotProjectModule() */) containingModule.dependencies.add(new SimpleDependency(Dependency.Role.LIBRARY, MavenCoord.fromGAVPC(mvnProject.getMavenIdentifier()))); return null; } MavenCoord modulePomCoords = new MavenCoord(); modulePomCoords.setGroupId(mavCtx.getUnifiedGroupId()); modulePomCoords.setVersion(mavCtx.getUnifiedVersion()); final String artifactId = deriveAppropriateArtifactId(projectModel); modulePomCoords.setArtifactId(artifactId); modulePomCoords.setPackaging(guessPackaging(projectModel)); Pom modulePom = new Pom(modulePomCoords); modulePom.role = Pom.ModuleRole.NORMAL; modulePom.parent = mavCtx.getRootPom(); mavCtx.getRootPom().submodules.put(artifactId, modulePom); mavCtx.getKnownSubmodules().add(modulePom); // Set up the dependency of the containing module on the contained module. E.g. EAR depends on WAR. if(containingModule != null) containingModule.dependencies.add(modulePom); // Nested archives // For now, only count with the modules of this app. There are likely more in the other apps. Set<ArchiveModel> nestedModules = new HashSet<>(); for (FileModel file : projectModel.getFileModelsNoDirectories()) { if(!(file instanceof ArchiveModel)) // TODO: Query for ArchiveModel directly. continue; // Known library -> simple dependency. if(file instanceof IdentifiedArchiveModel){ IdentifiedArchiveModel artifact = (IdentifiedArchiveModel) file; modulePom.dependencies.add(new SimpleDependency(Dependency.Role.LIBRARY, MavenCoord.from(artifact.getCoordinate()))); } // Unknown archives -> nested modules? -> local dependencies. else { nestedModules.add((ArchiveModel) file); } } // Nested modules already identified as ProjectModel for (ProjectModel subProject : projectModel.getChildProjects()) { Pom subModulePom = mavenizeModule(mavCtx, subProject, modulePom); if (subModulePom == null) continue; modulePom.dependencies.add(subModulePom); } // Nested module candidates. for (ArchiveModel nestedModule : nestedModules) { // TODO: Is it a submodule or a library? Does it appear in multiple applications? //Pom subModulePom = mavenizeModule(mavCtx, nestedModule, containingModule); //modulePom.dependencies.add(subModulePom.identification); } // TODO: MIGR-237 Determine and add the project internal dependencies (on other submodules, cross-app) // Queues? JNDI? CDI? REST + WS endpoints? // Needs to be done after initial mavenization of all apps to have their G:A:V. // TODO: Determine and add compile-time (API) dependencies (like, Java EE API's) // One big Java EE API vs. individual? // TODO: Remove the deps versions overriding the BOM. new FeatureBasedApiDependenciesDeducer(mavCtx).addAppropriateDependencies(projectModel, modulePom); return modulePom; } /** * Normalizes the name so it can be used as Maven artifactId or groupId. */ private static String normalizeDirName(String name) { if(name == null) return null; return name.toLowerCase().replaceAll("[^a-zA-Z0-9]", "-"); } /** * Tries to guess the packaging of the archive - whether it's an EAR, WAR, JAR. * Maybe not needed as we can rely on the suffix? */ private static String guessPackaging(ProjectModel projectModel) { String projectType = projectModel.getProjectType(); if (projectType != null) return projectType; LOG.warning("WINDUP-983 getProjectType() returned null for: " + projectModel.getRootFileModel().getPrettyPath()); String suffix = StringUtils.substringAfterLast(projectModel.getRootFileModel().getFileName(), "."); if ("jar war ear sar har ".contains(suffix+" ")){ projectModel.setProjectType(suffix); // FIXME: Remove when WINDUP-983 is fixed. return suffix; } // Should we try something more? Used APIs? What if it's a source? return "unknown"; } String deriveAppropriateArtifactId(ProjectModel projectModel) { String resultName = null; String name = projectModel.getName(); name: { if (name == null) break name; if (name.length() > 40) break name; if (name.contains(" ")) break name; resultName = name; } if (resultName == null) { resultName = projectModel.getRootFileModel().getFileName(); } resultName = removeVersion(resultName); resultName = normalizeDirName(resultName); if (resultName == null) resultName = "unknownName-" + RandomStringUtils.randomAlphanumeric(4); // See WINDUP-1015 if (resultName.length() > 40) resultName = resultName.substring(0,40) + "-" + RandomStringUtils.randomAlphanumeric(4); return resultName; } /** * Remove 1.0.0 from foo-1.0.0.jar */ String removeVersion(String resultName) { if(resultName == null) return null; // Regex test at http://fiddle.re/dnwhca return resultName.replaceFirst("[-_]\\d+(\\.\\d+)+(-SNAPSHOT|[.-_](?i:CR|RC|GA|Alpha|Beta|b|milestone|m|Final|RELEASE)[.-_]?\\d+|-incubating)?(?=[-_.]\\w+)", ""); } /** * Sorts the submodules of given Pom so that their cross-dependencies are satisfied if built in that order. * TODO - MIGR-236. */ private OrderedMap<String, Pom> sortSubmodulesToReflectDependencies(Pom pom) { Set<MavenCoord> dependenciesMet = new HashSet(); dependenciesMet.add(pom.coord); SortedSet<MavenCoord> dependenciesSatisfied = new TreeSet<>(); for (Dependency dep : pom.dependencies) { // Traverse the tree, depth-first, take items at node exit. } throw new UnsupportedOperationException("Not implemented yet."); } /** * Context of the mavenization - things to carry around. */ static class MavenizationContext { private Path mavenizedBaseDir; private Pom rootPom; private Set<Pom> knownSubmodules = new HashSet<>(); private String unifiedVersion; private String unifiedGroupId; private String unifiedAppName; private Pom bom; // BOM shared by all other submodules. private GraphContext graphContext; private Pom rootAppPom; public Path getMavenizedBaseDir() { return mavenizedBaseDir; } public Pom getRootPom() { return rootPom; } public Set<Pom> getKnownSubmodules() { return knownSubmodules; } public String getUnifiedVersion() { return unifiedVersion; } public String getUnifiedGroupId() { return unifiedGroupId; } public String getUnifiedAppName() { return unifiedAppName; } public Pom getBom() { return bom; } public GraphContext getGraphContext() { return graphContext; } public Pom getRootAppPom() { return rootAppPom; } public void setRootAppPom(Pom rootAppPom) { this.rootAppPom = rootAppPom; } } /** * Used to determine which BOM to take. * Currently we only have these tags: eap, eap7. */ private Set<String> getTargetTechnologies() { WindupConfigurationModel wc = grCtx.getUnique(WindupConfigurationModel.class); Iterable<TechnologyReferenceModel> targetTechnologies = wc.getTargetTechnologies(); Set<String> techs = new HashSet<>(); for (TechnologyReferenceModel tech : targetTechnologies) { techs.add(tech.getTechnologyID()); } return techs; } }