diff --git a/src/main/java/org/codehaus/mojo/flatten/FlattenMojo.java b/src/main/java/org/codehaus/mojo/flatten/FlattenMojo.java index dd23790c..818b28c2 100644 --- a/src/main/java/org/codehaus/mojo/flatten/FlattenMojo.java +++ b/src/main/java/org/codehaus/mojo/flatten/FlattenMojo.java @@ -367,6 +367,16 @@ public class FlattenMojo @Parameter( defaultValue = "${session}", readonly = true, required = true ) private MavenSession session; + /** + * The core maven model readers/writers are discarding the comments of the pom.xml. + * By setting keepCommentsInPom to true the current comments are moved to the flattened pom.xml. + * Default value is false (= not re-adding comments). + * + * @since 1.3.0 + */ + @Parameter( property = "flatten.dependency.keepComments", required = false , defaultValue = "false") + private boolean keepCommentsInPom; + @Component private DependencyResolver dependencyResolver; @@ -397,11 +407,15 @@ public void execute() getLog().info( "Generating flattened POM of project " + this.project.getId() + "..." ); File originalPomFile = this.project.getFile(); + KeepCommentsInPom commentsOfOriginalPomFile = null; + if (keepCommentsInPom) { + commentsOfOriginalPomFile = KeepCommentsInPom.create(getLog(), originalPomFile); + } Model flattenedPom = createFlattenedPom( originalPomFile ); String headerComment = extractHeaderComment( originalPomFile ); File flattenedPomFile = getFlattenedPomFile(); - writePom( flattenedPom, flattenedPomFile, headerComment ); + writePom( flattenedPom, flattenedPomFile, headerComment , commentsOfOriginalPomFile); if ( isUpdatePomFile() ) { @@ -409,7 +423,9 @@ public void execute() } } - /** + + + /** * This method extracts the XML header comment if available. * * @param xmlFile is the XML {@link File} to parse. @@ -445,7 +461,7 @@ protected String extractHeaderComment( File xmlFile ) * before root tag). May be null if not present and to be omitted in target POM. * @throws MojoExecutionException if the operation failed (e.g. due to an {@link IOException}). */ - protected void writePom( Model pom, File pomFile, String headerComment ) + protected void writePom( Model pom, File pomFile, String headerComment, KeepCommentsInPom anOriginalCommentsPath ) throws MojoExecutionException { @@ -483,10 +499,16 @@ protected void writePom( Model pom, File pomFile, String headerComment ) getLog().warn( "POM XML post-processing failed: no project tag found!" ); } } - writeStringToFile( buffer.toString(), pomFile, pom.getModelEncoding() ); + String xmlString; + if (anOriginalCommentsPath == null) { + xmlString = buffer.toString(); + } else { + xmlString = anOriginalCommentsPath.restoreOriginalComments(buffer.toString(), pom.getModelEncoding()); + } + writeStringToFile( xmlString, pomFile, pom.getModelEncoding() ); } - /** + /** * Writes the given data to the given file using the specified encoding. * * @param data is the {@link String} to write. diff --git a/src/main/java/org/codehaus/mojo/flatten/KeepCommentsInPom.java b/src/main/java/org/codehaus/mojo/flatten/KeepCommentsInPom.java new file mode 100644 index 00000000..3c68ea1b --- /dev/null +++ b/src/main/java/org/codehaus/mojo/flatten/KeepCommentsInPom.java @@ -0,0 +1,230 @@ +package org.codehaus.mojo.flatten; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.logging.Log; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.w3c.dom.bootstrap.DOMImplementationRegistry; +import org.w3c.dom.ls.DOMImplementationLS; +import org.w3c.dom.ls.LSOutput; +import org.w3c.dom.ls.LSSerializer; +import org.xml.sax.SAXException; + +/** + * Helper class to keep the comments how they have been in the original pom.xml + * While reading with {@link org.apache.maven.model.io.xpp3.MavenXpp3Writer} the comments are not placed into the {@link org.apache.maven.model.Model} + * and so {@link org.apache.maven.model.io.xpp3.MavenXpp3Writer} is not able to re-write those comments. + * + * Workaround (maybe until core is fixed) is to remember all the comments and restore them after MavenXpp3Writer has created the new flattened pom.xml. + * + * Current restriction on non-unique child nodes is that this class finds the node back due to the position in the file, + * that may lead to mis-re-added comments e.g. on multiple added dependencies + * (but for e.g. resolveCiFriendliesOnly the nodes keep stable) + * + */ +class KeepCommentsInPom { + + /** + * Create an instance with collected current comments of the passed pom.xml file. + */ + static KeepCommentsInPom create(Log aLog, File aOriginalPomFile) throws MojoExecutionException { + KeepCommentsInPom tempKeepCommentsInPom = new KeepCommentsInPom(); + tempKeepCommentsInPom.setLog(aLog); + tempKeepCommentsInPom.loadComments(aOriginalPomFile); + return tempKeepCommentsInPom; + } + + private Log log; + + /** + * The unique path list for an original node (the comments are stored via the referenced previousSibling) + */ + private Map commentsPaths; + + + /** + * + */ + KeepCommentsInPom() { + super(); + } + + /** + * load all current comments and text fragments from xml file + * @param anOriginalPomFile the pom.xml + */ + private void loadComments(File anOriginalPomFile) throws MojoExecutionException { + commentsPaths = new HashMap<>(); + DocumentBuilderFactory tempDBF = DocumentBuilderFactory.newInstance(); + DocumentBuilder tempDB; + try { + tempDB = tempDBF.newDocumentBuilder(); + Document tempPom = tempDB.parse(anOriginalPomFile); + Node tempNode = tempPom.getDocumentElement(); + walkOverNodes(tempNode,".",(node,nodePath)-> { + // collectNodesByPathNames + commentsPaths.put(nodePath,node); + }); + } catch (ParserConfigurationException | SAXException | IOException e) { + throw new MojoExecutionException("Cannot load comments from "+anOriginalPomFile, e); + } + } + + /** + * Walk over the pom hierarchy of the Document. + * @param Node the current Node + * @param String the unique path in the parent + * @param aConsumer Function to be called with the toBeCollected/found node. + */ + private void walkOverNodes(Node aNode, String aParentPath, BiConsumer aConsumer) { + String tempNodeName = aNode.getNodeName(); + if (log.isDebugEnabled()) { + log.debug("walkOverNodes: aParentPath="+aParentPath+" tempNodeName="+tempNodeName); + } + String tempNodePath = aParentPath+"\t"+tempNodeName; + aConsumer.accept(aNode, tempNodePath); + NodeList tempChilds = aNode.getChildNodes(); + // Copy the childs as aConsumer may change the node sequence (add a comment) + List tempCopiedChilds = new ArrayList<>(); + Map tempChildWithSameName = new HashMap<>(); + for (int i=0;i tempChildWithSameNameCounters = new HashMap<>(); + for (int i=0;i 1) { + Integer tempChildWithSameNameCounter = tempChildWithSameNameCounters.get(tempChildNodeName); + if (tempChildWithSameNameCounter == null) { + tempChildWithSameNameCounter = 1; + } else { + tempChildWithSameNameCounter += 1; + } + tempChildWithSameNameCounters.put(tempChildNodeName,tempChildWithSameNameCounter); + // add a counter to find back the correct node. + walkOverNodes(tempCopiedChild, tempNodePath+"\t"+tempChildWithSameNameCounter, aConsumer); + } else { + // unique child names + walkOverNodes(tempCopiedChild, tempNodePath, aConsumer); + } + } + } + + + /** + * @param String the XML written by {@link org.apache.maven.model.io.xpp3.MavenXpp3Writer} + */ + public String restoreOriginalComments(String anXml, String aModelEncoding) throws MojoExecutionException { + DocumentBuilderFactory tempDBF = DocumentBuilderFactory.newInstance(); + DocumentBuilder tempDB; + try { + tempDB = tempDBF.newDocumentBuilder(); + String tempEncoding = aModelEncoding==null ? "UTF-8" : aModelEncoding; // default encoding UTF-8 when nothing in pom model. + Document tempPom = tempDB.parse(new ByteArrayInputStream(anXml.getBytes(tempEncoding))); + Node tempNode = tempPom.getDocumentElement(); + walkOverNodes(tempNode,".",(newNode,nodePath)->{ + Node tempOriginalNode = commentsPaths.get(nodePath); + if (tempOriginalNode != null) { + String tempOriginalNodeName = tempOriginalNode.getNodeName(); + if (tempOriginalNodeName.equals(newNode.getNodeName())) { + // found matching node + Node tempRefChild = newNode; + Node tempPotentialCommentOrText = tempOriginalNode.getPreviousSibling(); + while (tempPotentialCommentOrText != null && tempPotentialCommentOrText.getNodeType() == Node.TEXT_NODE) { + // skip text in the original xml node + tempPotentialCommentOrText = tempPotentialCommentOrText.getPreviousSibling(); + } + while (tempPotentialCommentOrText != null && tempPotentialCommentOrText.getNodeType() == Node.COMMENT_NODE) { + // copy the node to be able to call previoussibling for next element + Node tempRefPrevious = tempRefChild.getPreviousSibling(); + String tempWhitespaceTextBeforeRefNode = null; + if (tempRefPrevious != null && tempRefPrevious.getNodeType()==Node.TEXT_NODE) { + tempWhitespaceTextBeforeRefNode = tempRefPrevious.getNodeValue(); + } + Node tempNewComment; + tempNewComment = tempPom.createComment(tempPotentialCommentOrText.getNodeValue()); + tempRefChild.getParentNode().insertBefore(tempNewComment, tempRefChild); + // copy the whitespaces between comment and refNode + if (tempWhitespaceTextBeforeRefNode!=null) { + tempRefChild.getParentNode().insertBefore(tempPom.createTextNode(tempWhitespaceTextBeforeRefNode), tempRefChild); + } + + tempRefChild=tempNewComment; + + tempPotentialCommentOrText = tempPotentialCommentOrText.getPreviousSibling(); + while (tempPotentialCommentOrText != null && tempPotentialCommentOrText.getNodeType() == Node.TEXT_NODE) { + // skip text in the original xml node + tempPotentialCommentOrText = tempPotentialCommentOrText.getPreviousSibling(); + } + } + } + } + }); + return writeDocumentToString(tempPom); + } catch (ParserConfigurationException | SAXException | IOException | ClassNotFoundException | InstantiationException | IllegalAccessException | ClassCastException e) { + throw new MojoExecutionException("Cannot add comments", e); + } + } + + /** + * Use an LSSerializer to keep whitespaces added by MavenXpp3Writer + * @param Document the pom to write to String. + */ + private String writeDocumentToString(Document aPom) throws ClassNotFoundException, InstantiationException, IllegalAccessException { + DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance(); + DOMImplementationLS impl = (DOMImplementationLS) registry.getDOMImplementation("LS"); + LSOutput output = impl.createLSOutput(); + output.setEncoding("UTF-8"); + ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + output.setByteStream(outStream ); + LSSerializer writer = impl.createLSSerializer(); + writer.write(aPom, output); + return new String(outStream.toByteArray()); + } + + /** + * @see #log + */ + public Log getLog() { + return log; + } + + /** + * @see #log + */ + public void setLog(Log aLog) { + log = aLog; + } + + + + +} diff --git a/src/test/java/org/codehaus/mojo/flatten/KeepCommentsInPomTest.java b/src/test/java/org/codehaus/mojo/flatten/KeepCommentsInPomTest.java new file mode 100644 index 00000000..a3093b30 --- /dev/null +++ b/src/test/java/org/codehaus/mojo/flatten/KeepCommentsInPomTest.java @@ -0,0 +1,115 @@ +package org.codehaus.mojo.flatten; + +import static org.junit.Assert.*; + +/* + * 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. + */ + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.io.IOUtils; +import org.apache.maven.plugin.testing.MojoRule; +import org.apache.maven.project.MavenProject; +import org.codehaus.plexus.configuration.DefaultPlexusConfiguration; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +/** + * Test-Case for {@link FlattenMojo}. + * + */ +public class KeepCommentsInPomTest { + + private static final String PATH = "src/test/resources/keep-comments-in-pom/"; + private static final String TEST_TARGET_PATH = "target/test/resources/keep-comments-in-pom/"; + private static final String FLATTENED_POM = TEST_TARGET_PATH + ".flattened-pom.xml"; + private static final String EXPECTED_FLATTENED_POM = PATH + "expected-flattened-pom.xml"; + /** + * Expected result since jdk11 with updated xml header and properties sequence. + */ + private static final String EXPECTED_FLATTENED_POM_JDK11 = PATH + "expected-flattened-pom-jdk11.xml"; + + @Rule + public MojoRule rule = new MojoRule(); + + @Before + public void setup() { + new File(TEST_TARGET_PATH).mkdirs(); + } + /** + * Test method to check that profile activation file is not interpolated. + * + * @throws Exception if something goes wrong. + */ + @Test + public void keepsProfileActivationFile() throws Exception { + MavenProject project = rule.readMavenProject( new File( PATH ) ); + FlattenMojo flattenMojo = (FlattenMojo) rule.lookupConfiguredMojo( project, "flatten" ); + + DefaultPlexusConfiguration tempPluginConfiguration = new DefaultPlexusConfiguration("test"); + tempPluginConfiguration.addChild("outputDirectory", TEST_TARGET_PATH); + tempPluginConfiguration.addChild("keepCommentsInPom", "true"); + rule.configureMojo(flattenMojo, tempPluginConfiguration); + + // execute writes new FLATTENED_POM + flattenMojo.execute(); + + String tempExpectedContent; + if (isJdk8()) { + tempExpectedContent = getContent(EXPECTED_FLATTENED_POM); + } else { + tempExpectedContent = getContent(EXPECTED_FLATTENED_POM_JDK11); + } + String tempActualContent = getContent(FLATTENED_POM); + assertEquals("Expected POM does not match, see "+FLATTENED_POM, tempExpectedContent,tempActualContent); + + } + + /** + * Check runtime version. + * + * @return true when runtime is JDK11 + */ + private boolean isJdk8() { + // With Java 9 can be switched to java.lang.Runtime.version() + String tempPropertyVersion = System.getProperty("java.version"); + if (tempPropertyVersion.startsWith("1.8.")) { + return true; + } + return false; + } + /** + * + */ + private String getContent(String aPomFile) throws IOException { + String tempString; + try (InputStream tempIn = new FileInputStream(aPomFile)) { + tempString = IOUtils.toString(tempIn); + } + // remove platform dependent CR/LF + tempString = tempString.replaceAll("\r\n", "\n"); + return tempString; + } + + +} diff --git a/src/test/resources/keep-comments-in-pom/expected-flattened-pom-jdk11.xml b/src/test/resources/keep-comments-in-pom/expected-flattened-pom-jdk11.xml new file mode 100644 index 00000000..b8c740d6 --- /dev/null +++ b/src/test/resources/keep-comments-in-pom/expected-flattened-pom-jdk11.xml @@ -0,0 +1,57 @@ + + 4.0.0 + org.codehaus.mojo.flatten.its + resolve-properties-ci-do-not-interpolate-profile-activation-file + 1.2.3.4 + + test-propertyWithoutComment + + + test-propertyWithTwoComments + + 1.2.3.4 + + + verify + + + org.codehaus.mojo + flatten-maven-plugin + + resolveCiFriendliesOnly + + + + + maven-surefire-plugin + + + **/TestCircle.java + **/TestCircle2.java + + **/TestSquare.java + + **/TestSquare2.java + + + + + + + + + + + file.txt + + + + + multiline-profile + + + + + \ No newline at end of file diff --git a/src/test/resources/keep-comments-in-pom/expected-flattened-pom.xml b/src/test/resources/keep-comments-in-pom/expected-flattened-pom.xml new file mode 100644 index 00000000..6c5543bd --- /dev/null +++ b/src/test/resources/keep-comments-in-pom/expected-flattened-pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + org.codehaus.mojo.flatten.its + resolve-properties-ci-do-not-interpolate-profile-activation-file + 1.2.3.4 + + + 1.2.3.4 + test-propertyWithoutComment + + + test-propertyWithTwoComments + + + verify + + + org.codehaus.mojo + flatten-maven-plugin + + resolveCiFriendliesOnly + + + + + maven-surefire-plugin + + + **/TestCircle.java + **/TestCircle2.java + + **/TestSquare.java + + **/TestSquare2.java + + + + + + + + + + + file.txt + + + + + multiline-profile + + + + + \ No newline at end of file diff --git a/src/test/resources/keep-comments-in-pom/pom.xml b/src/test/resources/keep-comments-in-pom/pom.xml new file mode 100644 index 00000000..72a37723 --- /dev/null +++ b/src/test/resources/keep-comments-in-pom/pom.xml @@ -0,0 +1,62 @@ + + 4.0.0 + org.codehaus.mojo.flatten.its + resolve-properties-ci-do-not-interpolate-profile-activation-file + ${revision} + + + + 1.2.3.4 + test-propertyWithoutComment + + + test-propertyWithTwoComments + + + + verify + + + org.codehaus.mojo + flatten-maven-plugin + + resolveCiFriendliesOnly + + + + + maven-surefire-plugin + + + **/TestCircle.java + **/TestCircle2.java + + **/TestSquare.java + + **/TestSquare2.java + + + + + + + + + + + + file.txt + + + + + multiline-profile + + + + + +