| /* |
| * 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.camel.maven.packaging; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileReader; |
| import java.io.FileWriter; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.StringWriter; |
| import java.net.URL; |
| import java.nio.charset.StandardCharsets; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.Set; |
| import java.util.TreeSet; |
| import java.util.stream.Collectors; |
| |
| import javax.xml.XMLConstants; |
| import javax.xml.parsers.DocumentBuilder; |
| import javax.xml.parsers.DocumentBuilderFactory; |
| import javax.xml.transform.OutputKeys; |
| import javax.xml.transform.Transformer; |
| import javax.xml.transform.TransformerFactory; |
| import javax.xml.transform.dom.DOMSource; |
| import javax.xml.transform.stream.StreamResult; |
| import javax.xml.xpath.XPath; |
| import javax.xml.xpath.XPathConstants; |
| import javax.xml.xpath.XPathExpression; |
| import javax.xml.xpath.XPathFactory; |
| |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.Node; |
| import org.w3c.dom.NodeList; |
| |
| import freemarker.cache.URLTemplateLoader; |
| import freemarker.template.Configuration; |
| import freemarker.template.Template; |
| import freemarker.template.TemplateException; |
| import org.apache.commons.io.IOUtils; |
| import org.apache.maven.plugin.AbstractMojo; |
| import org.apache.maven.plugin.MojoExecutionException; |
| import org.apache.maven.plugin.MojoFailureException; |
| 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; |
| |
| /** |
| * Generate Spring Boot starter for the component |
| */ |
| @Mojo(name = "prepare-spring-boot-starter", threadSafe = true, |
| requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME, |
| requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME) |
| public class SpringBootStarterMojo extends AbstractMojo { |
| |
| private static final String[] IGNORE_MODULES = { |
| /* OSGi -> */ "camel-blueprint", "camel-core-osgi", "camel-eventadmin", "camel-paxlogging", |
| /* extended core */ "camel-attachments", |
| /* Java EE -> */ "camel-cdi", |
| /* MicroProfile -> */ "camel-microprofile-config", |
| /* MicroProfile -> */ "camel-microprofile-health", |
| /* MicroProfile -> */ "camel-microprofile-metrics", |
| /* deprecated (and not working perfectly) -> */ "camel-swagger", "camel-ibatis", |
| /* currently incompatible */ "camel-spark-rest", |
| /* others (not managed) -> */ "camel-core-xml"}; |
| |
| private static final boolean IGNORE_TEST_MODULES = true; |
| |
| private static final String GENERATED_SECTION_START = "START OF GENERATED CODE"; |
| private static final String GENERATED_SECTION_START_COMMENT = "<!--" + GENERATED_SECTION_START + "-->"; |
| private static final String GENERATED_SECTION_END = "END OF GENERATED CODE"; |
| private static final String GENERATED_SECTION_END_COMMENT = "<!--" + GENERATED_SECTION_END + "-->"; |
| |
| /** |
| * The maven project. |
| */ |
| @Parameter(property = "project", required = true, readonly = true) |
| protected MavenProject project; |
| |
| /** |
| * Allows using the existing pom.xml file if present. |
| */ |
| @Parameter(property = "reuseExistingPom", defaultValue = "true") |
| protected boolean reuseExistingPom; |
| |
| /** |
| * The project directory |
| */ |
| @Parameter(defaultValue = "${basedir}") |
| protected File baseDir; |
| |
| @Override |
| public void execute() throws MojoExecutionException, MojoFailureException { |
| |
| if (!isStarterAllowed()) { |
| getLog().info("Spring-Boot-Starter: starter not allowed for module " + project.getArtifactId() + ": skipping."); |
| return; |
| } |
| |
| try { |
| // create the starter directory |
| File starterDir = starterDir(); |
| getLog().info("Spring-Boot-Starter: starter dir for the component is: " + starterDir.getAbsolutePath()); |
| |
| if (!starterDir.exists()) { |
| starterDir.mkdirs(); |
| } |
| |
| |
| |
| // create the base pom.xml |
| Document pom = createBasePom(); |
| |
| // Apply changes to the starter pom |
| fixExcludedDependencies(pom); |
| fixAdditionalDependencies(pom); |
| fixAdditionalRepositories(pom); |
| |
| // Write the starter pom |
| File pomFile = new File(starterDir, "pom.xml"); |
| writeXmlFormatted(pom, pomFile); |
| |
| // write LICENSE, USAGE and spring.provides files |
| writeStaticFiles(); |
| writeSpringProvides(); |
| |
| // synchronized all starters with their parent pom 'modules' section |
| synchronizeParentPom(); |
| |
| } catch (Exception e) { |
| throw new MojoFailureException("Unable to create starter", e); |
| } |
| |
| } |
| |
| private File starterDir() { |
| return SpringBootHelper.starterDir(baseDir, project.getArtifactId()); |
| } |
| |
| private File allStartersDir() { |
| return SpringBootHelper.allStartersDir(baseDir); |
| } |
| |
| private void fixAdditionalDependencies(Document pom) throws Exception { |
| Properties properties = new Properties(); |
| properties.load(getClass().getResourceAsStream("/spring-boot-fix-dependencies.properties")); |
| |
| Set<String> deps = new TreeSet<>(); |
| deps.addAll(csvToSet(properties.getProperty(project.getArtifactId()))); |
| |
| |
| Set<String> globalProps = csvToSet(properties.getProperty("global")); |
| boolean inGlobal = false; |
| for (String gp : globalProps) { |
| String[] comps = gp.split("\\:"); |
| String stdName = (comps[0] + ":" + comps[1]).replace("-starter", ""); |
| String camelGav = project.getGroupId() + ":" + project.getArtifactId(); |
| String camelSpringBootGav = project.getGroupId() + ".springboot:" + project.getArtifactId(); |
| if (stdName.equals(camelGav) || stdName.equals(camelSpringBootGav)) { |
| inGlobal = true; |
| break; |
| } |
| } |
| |
| if (!inGlobal) { |
| // add global properties for all modules not in global properties |
| deps.addAll(globalProps); |
| } |
| |
| if (deps.size() > 0) { |
| getLog().debug("The following dependencies will be added to the starter: " + deps); |
| |
| XPath xpath = XPathFactory.newInstance().newXPath(); |
| Node dependencies = ((NodeList) xpath.compile("/project/dependencies").evaluate(pom, XPathConstants.NODESET)).item(0); |
| |
| if (deps.size() > 0) { |
| dependencies.appendChild(pom.createComment(GENERATED_SECTION_START)); |
| for (String dep : deps) { |
| Element dependency = pom.createElement("dependency"); |
| dependencies.appendChild(dependency); |
| |
| String[] comps = dep.split("\\:"); |
| String groupIdStr = comps[0]; |
| String artifactIdStr = comps[1]; |
| String versionStr = comps.length > 2 ? comps[2] : null; |
| |
| Element groupId = pom.createElement("groupId"); |
| groupId.setTextContent(groupIdStr); |
| dependency.appendChild(groupId); |
| |
| Element artifactId = pom.createElement("artifactId"); |
| artifactId.setTextContent(artifactIdStr); |
| dependency.appendChild(artifactId); |
| |
| if (versionStr != null) { |
| Element version = pom.createElement("version"); |
| version.setTextContent(versionStr); |
| dependency.appendChild(version); |
| } |
| |
| } |
| dependencies.appendChild(pom.createComment(GENERATED_SECTION_END)); |
| } |
| } |
| } |
| |
| private void fixAdditionalRepositories(Document pom) throws Exception { |
| if (project.getFile() != null) { |
| DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); |
| dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, Boolean.TRUE); |
| dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); |
| DocumentBuilder builder = dbf.newDocumentBuilder(); |
| Document originalPom = builder.parse(project.getFile()); |
| |
| XPath xpath = XPathFactory.newInstance().newXPath(); |
| Node repositories = (Node) xpath.compile("/project/repositories").evaluate(originalPom, XPathConstants.NODE); |
| if (repositories != null) { |
| pom.getDocumentElement().appendChild(pom.createComment(GENERATED_SECTION_START)); |
| pom.getDocumentElement().appendChild(pom.importNode(repositories, true)); |
| pom.getDocumentElement().appendChild(pom.createComment(GENERATED_SECTION_END)); |
| } |
| } else { |
| getLog().warn("Cannot access the project pom file to retrieve repositories"); |
| } |
| } |
| |
| private Set<String> csvToSet(String csv) { |
| if (csv == null || csv.trim().length() == 0) { |
| return new TreeSet<>(); |
| } |
| |
| Set<String> set = new TreeSet<>(); |
| for (String s : csv.split(",")) { |
| set.add(s.trim()); |
| } |
| return set; |
| } |
| |
| private void fixExcludedDependencies(Document pom) throws Exception { |
| Set<String> loggingImpl = new HashSet<>(); |
| |
| loggingImpl.add("commons-logging:commons-logging"); |
| |
| loggingImpl.add("log4j:log4j"); |
| loggingImpl.add("log4j:apache-log4j-extras"); |
| |
| // removing also the default implementation |
| loggingImpl.add("ch.qos.logback:logback-core"); |
| loggingImpl.add("ch.qos.logback:logback-classic"); |
| |
| loggingImpl.add("org.apache.logging.log4j:log4j"); |
| loggingImpl.add("org.apache.logging.log4j:log4j-jcl"); |
| loggingImpl.add("org.apache.logging.log4j:log4j-core"); |
| loggingImpl.add("org.apache.logging.log4j:log4j-slf4j-impl"); |
| |
| loggingImpl.add("org.slf4j:slf4j-jcl"); |
| loggingImpl.add("org.slf4j:slf4j-jdk14"); |
| loggingImpl.add("org.slf4j:slf4j-log4j12"); |
| loggingImpl.add("org.slf4j:slf4j-log4j13"); |
| loggingImpl.add("org.slf4j:slf4j-nop"); |
| loggingImpl.add("org.slf4j:slf4j-simple"); |
| |
| // excluded dependencies |
| Set<String> configExclusions = new HashSet<>(); |
| Properties properties = new Properties(); |
| properties.load(getClass().getResourceAsStream("/spring-boot-fix-dependencies.properties")); |
| String artExcl = properties.getProperty("exclude_" + project.getArtifactId()); |
| getLog().debug("Configured exclusions: " + artExcl); |
| if (artExcl != null && artExcl.trim().length() > 0) { |
| for (String dep : artExcl.split(",")) { |
| getLog().debug("Adding configured exclusion: " + dep); |
| configExclusions.add(dep); |
| } |
| } |
| |
| Set<String> libsToRemove = new TreeSet<>(); |
| libsToRemove.addAll(loggingImpl); |
| libsToRemove.addAll(configExclusions); |
| libsToRemove = filterIncludedArtifacts(libsToRemove); |
| |
| if (libsToRemove.size() > 0) { |
| getLog().info("Spring-Boot-Starter: the following dependencies will be removed from the starter: " + libsToRemove); |
| |
| XPath xpath = XPathFactory.newInstance().newXPath(); |
| Node dependency = ((NodeList) xpath.compile("/project/dependencies/dependency[artifactId/text() = '" + project.getArtifactId() + "']").evaluate(pom, XPathConstants.NODESET)).item(0); |
| |
| Element exclusions = pom.createElement("exclusions"); |
| |
| dependency.appendChild(pom.createComment(GENERATED_SECTION_START)); |
| dependency.appendChild(exclusions); |
| dependency.appendChild(pom.createComment(GENERATED_SECTION_END)); |
| |
| for (String lib : libsToRemove) { |
| String groupIdStr = lib.split("\\:")[0]; |
| String artifactIdStr = lib.split("\\:")[1]; |
| |
| Element exclusion = pom.createElement("exclusion"); |
| |
| Element groupId = pom.createElement("groupId"); |
| groupId.setTextContent(groupIdStr); |
| exclusion.appendChild(groupId); |
| |
| Element artifactId = pom.createElement("artifactId"); |
| artifactId.setTextContent(artifactIdStr); |
| exclusion.appendChild(artifactId); |
| |
| exclusions.appendChild(exclusion); |
| } |
| } |
| } |
| |
| private Set<String> filterIncludedArtifacts(Set<String> artifacts) { |
| Set<String> included = new TreeSet<>(); |
| |
| included.addAll(project.getArtifactMap().keySet()); |
| included.retainAll(artifacts); |
| |
| return included; |
| } |
| |
| private void synchronizeParentPom() throws Exception { |
| File pomFile = new File(allStartersDir(), "pom.xml"); |
| DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); |
| Document pom = builder.parse(pomFile); |
| |
| XPath xpath = XPathFactory.newInstance().newXPath(); |
| Node modules = ((NodeList) xpath.compile("/project/modules").evaluate(pom, XPathConstants.NODESET)).item(0); |
| |
| // cleanup current modules |
| while (modules.hasChildNodes()) { |
| modules.removeChild(modules.getFirstChild()); |
| } |
| |
| for (File starterDir : Arrays |
| .asList(allStartersDir().listFiles((f, n) -> |
| (new File(f, n)).isDirectory() && n.endsWith(SpringBootHelper.STARTER_SUFFIX) && (new File(new File(f, n), "pom.xml").exists()))).stream().sorted() |
| .collect(Collectors.toList())) { |
| Node module = pom.createElement("module"); |
| module.setTextContent(starterDir.getName()); |
| modules.appendChild(module); |
| } |
| |
| writeXmlFormatted(pom, pomFile); |
| } |
| |
| private Document createBasePom() throws Exception { |
| Document pom = null; |
| if (reuseExistingPom) { |
| pom = createBasePomFromExisting(); |
| } |
| if (pom == null) { |
| pom = createBasePomFromScratch(); |
| } |
| |
| return pom; |
| } |
| |
| private Document createBasePomFromExisting() { |
| try { |
| File pomFile = new File(starterDir(), "pom.xml"); |
| if (pomFile.exists()) { |
| try (InputStream in = new FileInputStream(pomFile)) { |
| String content = IOUtils.toString(in, StandardCharsets.UTF_8); |
| boolean editablePom = content.contains(GENERATED_SECTION_START_COMMENT); |
| if (editablePom) { |
| content = removeGeneratedSections(content, 10); |
| DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); |
| |
| Document pom; |
| try (InputStream contentIn = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))) { |
| pom = builder.parse(contentIn); |
| } |
| |
| getLog().debug("Reusing the existing pom.xml for the starter"); |
| return pom; |
| } |
| } |
| } |
| } catch (Exception e) { |
| getLog().warn("Cannot use the existing pom.xml file", e); |
| } |
| |
| return null; |
| } |
| |
| private String removeGeneratedSections(String pom, int maxNumber) { |
| if (maxNumber > 0 && pom.contains(GENERATED_SECTION_START_COMMENT)) { |
| int start = pom.indexOf(GENERATED_SECTION_START_COMMENT); |
| int end = pom.indexOf(GENERATED_SECTION_END_COMMENT); |
| if (end <= start) { |
| throw new IllegalArgumentException("Generated sections inside the xml document are not well-formed"); |
| } |
| |
| String newPom = pom.substring(0, start) + pom.substring(end + GENERATED_SECTION_END_COMMENT.length()); |
| return removeGeneratedSections(newPom, maxNumber - 1); |
| } |
| |
| return pom; |
| } |
| |
| private Document createBasePomFromScratch() throws Exception { |
| getLog().info("Creating a new pom.xml for the starter from scratch"); |
| |
| Template pomTemplate = getTemplate("spring-boot-starter-template-pom.template"); |
| Map<String, String> props = new HashMap<>(); |
| props.put("version", project.getVersion()); |
| props.put("componentId", getComponentId()); |
| props.put("componentName", project.getName()); |
| props.put("componentDescription", project.getDescription()); |
| |
| StringWriter sw = new StringWriter(); |
| pomTemplate.process(props, sw); |
| |
| String xml = sw.toString(); |
| ByteArrayInputStream bin = new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)); |
| |
| DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); |
| Document pom = builder.parse(bin); |
| return pom; |
| } |
| |
| private void writeStaticFiles() throws IOException { |
| String notice; |
| String license; |
| try (InputStream isNotice = getClass().getResourceAsStream("/spring-boot-starter-NOTICE.txt"); |
| InputStream isLicense = getClass().getResourceAsStream("/spring-boot-starter-LICENSE.txt")) { |
| notice = IOUtils.toString(isNotice, StandardCharsets.UTF_8); |
| license = IOUtils.toString(isLicense, StandardCharsets.UTF_8); |
| } |
| |
| writeIfChanged(notice, new File(starterDir(), "src/main/resources/META-INF/NOTICE.txt")); |
| writeIfChanged(license, new File(starterDir(), "src/main/resources/META-INF/LICENSE.txt")); |
| } |
| |
| private void writeSpringProvides() throws IOException, TemplateException { |
| Template fileTemplate = getTemplate("spring-boot-starter-template-spring.provides"); |
| Map<String, String> props = new HashMap<>(); |
| props.put("artifactId", project.getArtifactId()); |
| |
| File outDir = new File(starterDir(), "src/main/resources/META-INF"); |
| outDir.mkdirs(); |
| File outFile = new File(outDir, "spring.provides"); |
| |
| StringWriter sw = new StringWriter(); |
| fileTemplate.process(props, sw); |
| sw.close(); |
| |
| writeIfChanged(sw.toString(), outFile); |
| } |
| |
| |
| private Template getTemplate(String name) throws IOException { |
| Configuration cfg = new Configuration(Configuration.getVersion()); |
| |
| cfg.setTemplateLoader(new URLTemplateLoader() { |
| @Override |
| protected URL getURL(String name) { |
| return SpringBootStarterMojo.class.getResource("/" + name); |
| } |
| }); |
| |
| cfg.setDefaultEncoding("UTF-8"); |
| Template template = cfg.getTemplate(name); |
| return template; |
| } |
| |
| |
| private boolean isStarterAllowed() { |
| |
| for (String ignored : IGNORE_MODULES) { |
| if (ignored.equals(project.getArtifactId())) { |
| getLog().debug("Component inside ignore list"); |
| return false; |
| } |
| } |
| |
| if (IGNORE_TEST_MODULES && (project.getArtifactId().startsWith("camel-test") |
| || project.getArtifactId().startsWith("camel-testcontainers"))) { |
| getLog().debug("Test components are ignored"); |
| return false; |
| } |
| |
| if (project.getPackaging() != null && !project.getPackaging().equals("jar")) { |
| getLog().debug("Ignored for wrong packaging"); |
| return false; |
| } |
| |
| // include 'camel-core' |
| if (baseDir.getName().equals("camel-core")) { |
| return true; |
| } |
| |
| // Build a starter for all components under the 'components' dir and include submodules ending with '-component' |
| if (baseDir.getParentFile().getName().equals("components") || baseDir.getName().endsWith("-component")) { |
| return true; |
| } |
| |
| if (baseDir.getName().equals("camel-jaxp")) { |
| return true; |
| } |
| |
| getLog().debug("Component directory mismatch"); |
| return false; |
| } |
| |
| |
| private String getComponentId() { |
| String componentName = project.getArtifact().getArtifactId(); |
| String componentId = componentName.replace("camel-", ""); |
| return componentId; |
| } |
| |
| private void writeXmlFormatted(Document pom, File destination) throws Exception { |
| XPathExpression xpath = XPathFactory.newInstance().newXPath().compile("//text()[normalize-space(.) = '']"); |
| NodeList emptyNodes = (NodeList) xpath.evaluate(pom, XPathConstants.NODESET); |
| |
| // Remove empty text nodes |
| for (int i = 0; i < emptyNodes.getLength(); i++) { |
| Node emptyNode = emptyNodes.item(i); |
| emptyNode.getParentNode().removeChild(emptyNode); |
| } |
| |
| pom.setXmlStandalone(true); |
| |
| TransformerFactory transformerFactory = TransformerFactory.newInstance(); |
| transformerFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, Boolean.TRUE); |
| Transformer transformer = transformerFactory.newTransformer(); |
| transformer.setOutputProperty(OutputKeys.INDENT, "yes"); |
| transformer.setOutputProperty(OutputKeys.METHOD, "xml"); |
| transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); |
| |
| DOMSource source = new DOMSource(pom); |
| |
| String content; |
| try (StringWriter out = new StringWriter()) { |
| StreamResult result = new StreamResult(out); |
| transformer.transform(source, result); |
| content = out.toString(); |
| } |
| |
| // Fix header formatting problem |
| content = content.replaceFirst("-->", "-->\n").replaceFirst("\\?><!--", "\\?>\n<!--"); |
| |
| writeIfChanged(content, destination); |
| } |
| |
| private void writeIfChanged(String content, File file) throws IOException { |
| boolean write = true; |
| |
| if (file.exists()) { |
| try (FileReader fr = new FileReader(file)) { |
| String oldContent = IOUtils.toString(fr); |
| if (!content.equals(oldContent)) { |
| getLog().debug("Writing new file " + file.getAbsolutePath()); |
| fr.close(); |
| } else { |
| getLog().debug("File " + file.getAbsolutePath() + " has been left unchanged"); |
| write = false; |
| } |
| } |
| } else { |
| // Create the structure |
| File parent = file.getParentFile(); |
| parent.mkdirs(); |
| } |
| |
| if (write) { |
| try (FileWriter fw = new FileWriter(file)) { |
| IOUtils.write(content, fw); |
| } |
| } |
| } |
| |
| } |