From b496e960a4590d434987a975ab879ae785877185 Mon Sep 17 00:00:00 2001 From: Stefan CORDES <50696194+ca-stefan-cordes@users.noreply.github.com> Date: Mon, 8 Aug 2022 17:13:37 +0200 Subject: [PATCH] Keep comments in .flattened-pom.xml #270 --- .../codehaus/mojo/flatten/FlattenMojo.java | 32 ++- .../mojo/flatten/KeepCommentsInPom.java | 269 ++++++++++++++++++ .../mojo/flatten/KeepCommentsInPomTest.java | 125 ++++++++ .../expected-flattened-pom-jdk11.xml | 57 ++++ .../expected-flattened-pom.xml | 58 ++++ .../resources/keep-comments-in-pom/pom.xml | 62 ++++ 6 files changed, 598 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/codehaus/mojo/flatten/KeepCommentsInPom.java create mode 100644 src/test/java/org/codehaus/mojo/flatten/KeepCommentsInPomTest.java create mode 100644 src/test/resources/keep-comments-in-pom/expected-flattened-pom-jdk11.xml create mode 100644 src/test/resources/keep-comments-in-pom/expected-flattened-pom.xml create mode 100644 src/test/resources/keep-comments-in-pom/pom.xml 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..c0aa5ed4 --- /dev/null +++ b/src/main/java/org/codehaus/mojo/flatten/KeepCommentsInPom.java @@ -0,0 +1,269 @@ +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 < tempChilds.getLength(); i++) + { + Node tempItem = tempChilds.item(i); + if (tempItem.getNodeType() != Node.TEXT_NODE && tempItem.getNodeType() != Node.COMMENT_NODE) + { + // Take real nodes to find them back by number + String tempChildNodeName = tempItem.getNodeName(); + Integer tempChildWithSameNameCount = tempChildWithSameName.get(tempChildNodeName); + if (tempChildWithSameNameCount == null) + { + tempChildWithSameNameCount = 1; + } else + { + tempChildWithSameNameCount += 1; + } + tempChildWithSameName.put(tempChildNodeName, tempChildWithSameNameCount); + tempCopiedChilds.add(tempItem); + } + } + Map tempChildWithSameNameCounters = new HashMap<>(); + for (int i = 0; i < tempCopiedChilds.size(); i++) + { + Node tempCopiedChild = tempCopiedChilds.get(i); + String tempChildNodeName = tempCopiedChild.getNodeName(); + if (tempChildWithSameName.get(tempChildNodeName) > 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..b00df593 --- /dev/null +++ b/src/test/java/org/codehaus/mojo/flatten/KeepCommentsInPomTest.java @@ -0,0 +1,125 @@ +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 + + + + + +