Skip to content

Commit

Permalink
keep comments in .flattened-pom.xml mojohaus#269
Browse files Browse the repository at this point in the history
  • Loading branch information
ca-stefan-cordes committed Apr 8, 2022
1 parent ea1cb42 commit d7aa757
Show file tree
Hide file tree
Showing 5 changed files with 333 additions and 10 deletions.
46 changes: 41 additions & 5 deletions src/main/java/org/codehaus/mojo/flatten/FlattenMojo.java
Expand Up @@ -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;

Expand Down Expand Up @@ -386,19 +393,25 @@ 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() )
{
this.project.setPomFile( flattenedPomFile );
}
}

/**


/**
* This method extracts the XML header comment if available.
*
* @param xmlFile is the XML {@link File} to parse.
Expand Down Expand Up @@ -434,7 +447,7 @@ protected String extractHeaderComment( File xmlFile )
* before root tag). May be <code>null</code> 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
{

Expand Down Expand Up @@ -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 <code>data</code> to the given <code>file</code> using the specified <code>encoding</code>.
*
* @param data is the {@link String} to write.
Expand Down Expand Up @@ -1213,6 +1232,23 @@ public boolean isUpdatePomFile()
}
}

/**
* @return <code>true</code> if the generated flattened POM shall have the comments of the original file
* <code>false</code> 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.
Expand Down
236 changes: 236 additions & 0 deletions 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<String,AtomicReference<Node>> 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<Node, AtomicReference<Node>> aConsumer) {
String tempNodeName = aNode.getNodeName();
if (log.isDebugEnabled()) {
log.debug("walkOverNodes: aParentPath="+aParentPath+" tempNodeName="+tempNodeName);
}
String tempNodePath = aParentPath+"\t"+tempNodeName;
AtomicReference<Node> 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<Node> tempCopiedChilds = new ArrayList<>();
Map<String,Integer> 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<String,Integer> 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,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;
}




}
Expand Up @@ -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
Expand Down
34 changes: 29 additions & 5 deletions src/test/resources/keep-comments-in-pom/expected-flattened-pom.xml
@@ -1,13 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.codehaus.mojo.flatten.its</groupId>
<artifactId>resolve-properties-ci-do-not-interpolate-profile-activation-file</artifactId>
<version>1.2.3.4</version>
<properties>
<!-- some nice comment -->
<!-- some nice comment -->
<revision>1.2.3.4</revision>
<propertyWithoutComment>test-propertyWithoutComment</propertyWithoutComment>
<!-- comment1 for propertyWithTwoComments -->
<!-- comment2 for propertyWithTwoComments -->
<propertyWithTwoComments>test-propertyWithTwoComments</propertyWithTwoComments>
</properties>
<build>
<defaultGoal>verify</defaultGoal>
Expand All @@ -19,16 +22,37 @@
<flattenMode>resolveCiFriendliesOnly</flattenMode>
</configuration>
</plugin>
<plugin>
<!-- plugin with multiple same node names -->
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludes>
<exclude>**/TestCircle.java</exclude>
<exclude>**/TestCircle2.java</exclude>
<!-- special info for TestSquare -->
<exclude>**/TestSquare.java</exclude>
<!-- special info for TestSquare2 -->
<exclude>**/TestSquare2.java</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<!-- another comment what the profile is doing -->
<!-- another comment what the profile is doing -->
<activation>
<file>
<exists>file.txt</exists>
</file>
</activation>
</profile>
<profile>
<id>multiline-profile</id>
<!-- comment with
multiple lines
in a profile -->
<activation/>
</profile>
</profiles>
</project>
</project>

0 comments on commit d7aa757

Please sign in to comment.