From d7aa7579563836f8be188e497a78eacb3f8be64b Mon Sep 17 00:00:00 2001 From: Stefan CORDES <50696194+ca-stefan-cordes@users.noreply.github.com> Date: Fri, 8 Apr 2022 19:03:56 +0200 Subject: [PATCH] keep comments in .flattened-pom.xml #269 --- .../codehaus/mojo/flatten/FlattenMojo.java | 46 +++- .../mojo/flatten/KeepCommentsInPom.java | 236 ++++++++++++++++++ .../mojo/flatten/KeepCommentsInPomTest.java | 1 + .../expected-flattened-pom.xml | 34 ++- .../resources/keep-comments-in-pom/pom.xml | 26 ++ 5 files changed, 333 insertions(+), 10 deletions(-) create mode 100644 src/main/java/org/codehaus/mojo/flatten/KeepCommentsInPom.java diff --git a/src/main/java/org/codehaus/mojo/flatten/FlattenMojo.java b/src/main/java/org/codehaus/mojo/flatten/FlattenMojo.java index 4042f6cf..03853872 100644 --- a/src/main/java/org/codehaus/mojo/flatten/FlattenMojo.java +++ b/src/main/java/org/codehaus/mojo/flatten/FlattenMojo.java @@ -356,6 +356,13 @@ 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. + */ + @Parameter( property = "flatten.dependency.keepComments", required = false ) + private Boolean keepCommentsInPom; + @Component private DependencyResolver dependencyResolver; @@ -386,11 +393,15 @@ public void execute() getLog().info( "Generating flattened POM of project " + this.project.getId() + "..." ); File originalPomFile = this.project.getFile(); + KeepCommentsInPom commentsOfOriginalPomFile = null; + if (isKeepCommentsInPom()) { + 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() ) { @@ -398,7 +409,9 @@ public void execute() } } - /** + + + /** * This method extracts the XML header comment if available. * * @param xmlFile is the XML {@link File} to parse. @@ -434,7 +447,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 { @@ -472,10 +485,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. @@ -1213,6 +1232,23 @@ public boolean isUpdatePomFile() } } + /** + * @return true if the generated flattened POM shall have the comments of the original file + * false will remove the comments. + */ + public boolean isKeepCommentsInPom() + { + + if ( this.keepCommentsInPom == null ) + { + return false; + } + else + { + return this.keepCommentsInPom.booleanValue(); + } + } + /** * This class is a simple SAX handler that extracts the first comment located before the root tag in an XML * document. 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..8b02303f --- /dev/null +++ b/src/main/java/org/codehaus/mojo/flatten/KeepCommentsInPom.java @@ -0,0 +1,236 @@ +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.concurrent.atomic.AtomicReference; +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,nodes)-> { + // collectNodesByPathNames + nodes.set(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; + AtomicReference tempNodesOnPath = commentsPaths.get(tempNodePath); + if (tempNodesOnPath == null) { + tempNodesOnPath = new AtomicReference<>(); + commentsPaths.put(tempNodePath,tempNodesOnPath); + } + aConsumer.accept(aNode, tempNodesOnPath); + 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,originalNodeRef)->{ + Node tempOriginalNode = originalNodeRef.get(); + 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 index 8fc03ab1..400d63b8 100644 --- a/src/test/java/org/codehaus/mojo/flatten/KeepCommentsInPomTest.java +++ b/src/test/java/org/codehaus/mojo/flatten/KeepCommentsInPomTest.java @@ -64,6 +64,7 @@ public void keepsProfileActivationFile() throws Exception { DefaultPlexusConfiguration tempPluginConfiguration = new DefaultPlexusConfiguration("test"); tempPluginConfiguration.addChild("outputDirectory", TEST_TARGET_PATH); + tempPluginConfiguration.addChild("keepCommentsInPom", "true"); rule.configureMojo(flattenMojo, tempPluginConfiguration); // execute writes new FLATTENED_POM 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 index 83742f89..6c5543bd 100644 --- a/src/test/resources/keep-comments-in-pom/expected-flattened-pom.xml +++ b/src/test/resources/keep-comments-in-pom/expected-flattened-pom.xml @@ -1,13 +1,16 @@ - + 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 @@ -19,16 +22,37 @@ 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 index d1d41a08..72a37723 100644 --- a/src/test/resources/keep-comments-in-pom/pom.xml +++ b/src/test/resources/keep-comments-in-pom/pom.xml @@ -8,6 +8,10 @@ 1.2.3.4 + test-propertyWithoutComment + + + test-propertyWithTwoComments @@ -20,6 +24,20 @@ resolveCiFriendliesOnly + + + maven-surefire-plugin + + + **/TestCircle.java + **/TestCircle2.java + + **/TestSquare.java + + **/TestSquare2.java + + + @@ -32,5 +50,13 @@ + + multiline-profile + + + +