JDomUtils.java
/*
* 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.maven.cling.invoker.mvnup.goals;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.codehaus.plexus.util.StringUtils;
import org.jdom2.Content;
import org.jdom2.Element;
import org.jdom2.Namespace;
import org.jdom2.Parent;
import org.jdom2.Text;
import static java.util.Arrays.asList;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.Indentation;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.ARTIFACT_ID;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.BUILD;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.CI_MANAGEMENT;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.CLASSIFIER;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.CONFIGURATION;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.CONTRIBUTORS;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.DEFAULT_GOAL;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.DEPENDENCIES;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.DEPENDENCY;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.DEPENDENCY_MANAGEMENT;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.DESCRIPTION;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.DEVELOPERS;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.DIRECTORY;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.DISTRIBUTION_MANAGEMENT;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.EXCLUSIONS;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.EXECUTIONS;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.EXTENSIONS;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.FINAL_NAME;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.GOALS;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.GROUP_ID;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.INCEPTION_YEAR;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.INHERITED;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.ISSUE_MANAGEMENT;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.LICENSES;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.MAILING_LISTS;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.MODEL_VERSION;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.MODULES;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.NAME;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.OPTIONAL;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.ORGANIZATION;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.OUTPUT_DIRECTORY;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PACKAGING;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PARENT;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGIN;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGINS;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGIN_MANAGEMENT;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGIN_REPOSITORIES;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PREREQUISITES;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PROFILES;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PROPERTIES;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.REPORTING;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.REPOSITORIES;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.SCM;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.SCOPE;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.SCRIPT_SOURCE_DIRECTORY;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.SOURCE_DIRECTORY;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.SYSTEM_PATH;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.TEST_OUTPUT_DIRECTORY;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.TEST_SOURCE_DIRECTORY;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.TYPE;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.URL;
import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.VERSION;
import static org.jdom2.filter.Filters.textOnly;
/**
* Utility class for JDOM operations.
*/
public class JDomUtils {
// Element ordering configuration
private static final Map<String, List<String>> ELEMENT_ORDER = new HashMap<>();
static {
// Project element order
ELEMENT_ORDER.put(
"project",
asList(
MODEL_VERSION,
"",
PARENT,
"",
GROUP_ID,
ARTIFACT_ID,
VERSION,
PACKAGING,
"",
NAME,
DESCRIPTION,
URL,
INCEPTION_YEAR,
ORGANIZATION,
LICENSES,
"",
DEVELOPERS,
CONTRIBUTORS,
"",
MAILING_LISTS,
"",
PREREQUISITES,
"",
MODULES,
"",
SCM,
ISSUE_MANAGEMENT,
CI_MANAGEMENT,
DISTRIBUTION_MANAGEMENT,
"",
PROPERTIES,
"",
DEPENDENCY_MANAGEMENT,
DEPENDENCIES,
"",
REPOSITORIES,
PLUGIN_REPOSITORIES,
"",
BUILD,
"",
REPORTING,
"",
PROFILES));
// Build element order
ELEMENT_ORDER.put(
BUILD,
asList(
DEFAULT_GOAL,
DIRECTORY,
FINAL_NAME,
SOURCE_DIRECTORY,
SCRIPT_SOURCE_DIRECTORY,
TEST_SOURCE_DIRECTORY,
OUTPUT_DIRECTORY,
TEST_OUTPUT_DIRECTORY,
EXTENSIONS,
"",
PLUGIN_MANAGEMENT,
PLUGINS));
// Plugin element order
ELEMENT_ORDER.put(
PLUGIN,
asList(
GROUP_ID,
ARTIFACT_ID,
VERSION,
EXTENSIONS,
EXECUTIONS,
DEPENDENCIES,
GOALS,
INHERITED,
CONFIGURATION));
// Dependency element order
ELEMENT_ORDER.put(
DEPENDENCY,
asList(GROUP_ID, ARTIFACT_ID, VERSION, CLASSIFIER, TYPE, SCOPE, SYSTEM_PATH, OPTIONAL, EXCLUSIONS));
}
private JDomUtils() {
// noop
}
/**
* Inserts a new child element to the given root element. The position where the element is inserted is calculated
* using the element order configuration. When no order is defined for the element, the new element is append as
* last element (before the closing tag of the root element). In the root element, the new element is always
* prepended by a text element containing a linebreak followed by the indentation characters. The indentation
* characters are (tried to be) detected from the root element (see {@link #detectIndentation(Element)} ).
*
* @param name the name of the new element.
* @param root the root element.
* @return the new element.
*/
public static Element insertNewElement(String name, Element root) {
return insertNewElement(name, root, calcNewElementIndex(name, root));
}
/**
* Inserts a new child element to the given root element at the given index.
* For details see {@link #insertNewElement(String, Element)}
*
* @param name the name of the new element.
* @param root the root element.
* @param index the index where the element should be inserted.
* @return the new element.
*/
public static Element insertNewElement(String name, Element root, int index) {
String indent = detectIndentation(root);
Element newElement = createElement(name, root.getNamespace());
// If the parent element only has minimal content (just closing tag indentation),
// we need to handle it specially to avoid creating whitespace-only lines
boolean parentHasMinimalContent = root.getContentSize() == 1
&& root.getContent(0) instanceof Text
&& ((Text) root.getContent(0)).getText().trim().isEmpty();
if (parentHasMinimalContent) {
// Remove the minimal content and let addAppropriateSpacing handle the formatting
root.removeContent();
index = 0; // Reset index since we removed content
}
root.addContent(index, newElement);
addAppropriateSpacing(root, index, name, indent);
// Ensure both the parent and new element have proper closing tag formatting
ensureProperClosingTagFormatting(root);
ensureProperClosingTagFormatting(newElement);
return newElement;
}
/**
* Creates a new element with proper formatting.
* This method ensures that both the opening and closing tags are properly indented.
*/
private static Element createElement(String name, Namespace namespace) {
Element newElement = new Element(name, namespace);
// Add minimal content to prevent self-closing tag and ensure proper formatting
// This will be handled by ensureProperClosingTagFormatting
newElement.addContent(new Text(""));
return newElement;
}
/**
* Adds appropriate spacing before the inserted element.
*/
private static void addAppropriateSpacing(Element root, int index, String elementName, String indent) {
// Find the preceding element name for spacing logic
String prependingElementName = "";
if (index > 0) {
Content prevContent = root.getContent(index - 1);
if (prevContent instanceof Element) {
prependingElementName = ((Element) prevContent).getName();
}
}
if (isBlankLineBetweenElements(prependingElementName, elementName, root)) {
// Add a completely empty line followed by proper indentation
// We need to be careful to ensure the empty line has no spaces
root.addContent(index, new Text("\n")); // End current line
root.addContent(index + 1, new Text("\n" + indent)); // Empty line + indentation for next element
} else {
root.addContent(index, new Text("\n" + indent));
}
}
/**
* Ensures that the parent element has proper closing tag formatting.
* This method checks if the last content of the element is properly indented
* and adds appropriate whitespace if needed.
*/
private static void ensureProperClosingTagFormatting(Element parent) {
List<Content> contents = parent.getContent();
// Get the parent's indentation level
String parentIndent = detectParentIndentation(parent);
// If the element is empty or only contains empty text nodes, handle it specially
if (contents.isEmpty()
|| (contents.size() == 1
&& contents.get(0) instanceof Text
&& ((Text) contents.get(0)).getText().trim().isEmpty())) {
// For empty elements, add minimal content to ensure proper formatting
// We add just a newline and parent indentation, which will be the closing tag line
parent.removeContent();
parent.addContent(new Text("\n" + parentIndent));
return;
}
// Check if the last content is a Text node with proper indentation
Content lastContent = contents.get(contents.size() - 1);
if (lastContent instanceof Text) {
String text = ((Text) lastContent).getText();
// If the last text doesn't end with proper indentation for the closing tag
if (!text.endsWith("\n" + parentIndent)) {
// If it's only whitespace, replace it; otherwise append
if (text.trim().isEmpty()) {
parent.removeContent(lastContent);
parent.addContent(new Text("\n" + parentIndent));
} else {
// Append proper indentation
parent.addContent(new Text("\n" + parentIndent));
}
}
} else {
// If the last content is not a text node, add proper indentation for closing tag
parent.addContent(new Text("\n" + parentIndent));
}
}
/**
* Detects the indentation level of the parent element.
*/
private static String detectParentIndentation(Element element) {
Parent parent = element.getParent();
if (parent instanceof Element) {
return detectIndentation((Element) parent);
}
return "";
}
/**
* Inserts a new content element with the given name and text content.
*
* @param parent the parent element
* @param name the name of the new element
* @param content the text content
* @return the new element
*/
public static Element insertContentElement(Element parent, String name, String content) {
Element element = insertNewElement(name, parent);
element.setText(content);
return element;
}
/**
* Detects the indentation used for a given element by analyzing its parent's content.
* This method examines the whitespace preceding the element to determine the indentation pattern.
* It supports different indentation styles (2 spaces, 4 spaces, tabs, etc.).
*
* @param element the element to analyze
* @return the detected indentation or a default indentation if none can be detected.
*/
public static String detectIndentation(Element element) {
// First try to detect from the current element
for (Iterator<Text> iterator = element.getContent(textOnly()).iterator(); iterator.hasNext(); ) {
String text = iterator.next().getText();
int lastLsIndex = StringUtils.lastIndexOfAny(text, new String[] {"\n", "\r"});
if (lastLsIndex > -1) {
String indent = text.substring(lastLsIndex + 1);
if (iterator.hasNext()) {
// This should be the indentation of a child element.
return indent;
} else {
// This should be the indentation of the elements end tag.
String baseIndent = detectBaseIndentationUnit(element);
return indent + baseIndent;
}
}
}
Parent parent = element.getParent();
if (parent instanceof Element) {
String baseIndent = detectBaseIndentationUnit(element);
return detectIndentation((Element) parent) + baseIndent;
}
return "";
}
/**
* Detects the base indentation unit used in the document by analyzing indentation patterns.
* This method traverses the document tree to find the most common indentation style.
*
* @param element any element in the document to analyze
* @return the detected base indentation unit (e.g., " ", " ", "\t")
*/
public static String detectBaseIndentationUnit(Element element) {
// Find the root element to analyze the entire document
Element root = element;
while (root.getParent() instanceof Element) {
root = (Element) root.getParent();
}
// Collect indentation samples from the document
Map<String, Integer> indentationCounts = new HashMap<>();
collectIndentationSamples(root, indentationCounts, "");
// Analyze the collected samples to determine the base unit
return analyzeIndentationPattern(indentationCounts);
}
/**
* Recursively collects indentation samples from the document tree.
*/
private static void collectIndentationSamples(
Element element, Map<String, Integer> indentationCounts, String parentIndent) {
for (Iterator<Text> iterator = element.getContent(textOnly()).iterator(); iterator.hasNext(); ) {
String text = iterator.next().getText();
int lastLsIndex = StringUtils.lastIndexOfAny(text, new String[] {"\n", "\r"});
if (lastLsIndex > -1) {
String indent = text.substring(lastLsIndex + 1);
if (iterator.hasNext() && !indent.isEmpty()) {
// This is indentation before a child element
if (indent.length() > parentIndent.length()) {
String indentDiff = indent.substring(parentIndent.length());
indentationCounts.merge(indentDiff, 1, Integer::sum);
}
}
}
}
// Recursively analyze child elements
for (Element child : element.getChildren()) {
String childIndent = detectIndentationForElement(element, child);
if (childIndent != null && childIndent.length() > parentIndent.length()) {
String indentDiff = childIndent.substring(parentIndent.length());
indentationCounts.merge(indentDiff, 1, Integer::sum);
collectIndentationSamples(child, indentationCounts, childIndent);
}
}
}
/**
* Detects the indentation used for a specific child element.
*/
private static String detectIndentationForElement(Element parent, Element child) {
int childIndex = parent.indexOf(child);
if (childIndex > 0) {
Content prevContent = parent.getContent(childIndex - 1);
if (prevContent instanceof Text) {
String text = ((Text) prevContent).getText();
int lastLsIndex = StringUtils.lastIndexOfAny(text, new String[] {"\n", "\r"});
if (lastLsIndex > -1) {
return text.substring(lastLsIndex + 1);
}
}
}
return null;
}
/**
* Analyzes the collected indentation patterns to determine the most likely base unit.
*/
private static String analyzeIndentationPattern(Map<String, Integer> indentationCounts) {
if (indentationCounts.isEmpty()) {
return Indentation.TWO_SPACES; // Default to 2 spaces
}
// Find the most common indentation pattern
String mostCommon = indentationCounts.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse(Indentation.TWO_SPACES);
// Validate and normalize the detected pattern
if (mostCommon.matches("^\\s+$")) { // Only whitespace characters
return mostCommon;
}
// If we have mixed patterns, try to find a common base unit
Set<String> patterns = indentationCounts.keySet();
// Check for common patterns
if (patterns.stream().anyMatch(p -> p.equals(Indentation.FOUR_SPACES))) {
return Indentation.FOUR_SPACES; // 4 spaces
}
if (patterns.stream().anyMatch(p -> p.equals(Indentation.TAB))) {
return Indentation.TAB; // Tab
}
if (patterns.stream().anyMatch(p -> p.equals(Indentation.TWO_SPACES))) {
return Indentation.TWO_SPACES; // 2 spaces
}
// Fallback to the most common pattern or default
return mostCommon.isEmpty() ? Indentation.TWO_SPACES : mostCommon;
}
/**
* Calculates the index where a new element with the given name should be inserted.
*/
private static int calcNewElementIndex(String elementName, Element parent) {
List<String> elementOrder = ELEMENT_ORDER.get(parent.getName());
if (elementOrder == null || elementOrder.isEmpty()) {
return parent.getContentSize();
}
int targetIndex = elementOrder.indexOf(elementName);
if (targetIndex == -1) {
return parent.getContentSize();
}
// Find the position to insert based on element order
List<Content> contents = parent.getContent();
for (int i = contents.size() - 1; i >= 0; i--) {
Content content = contents.get(i);
if (content instanceof Element element) {
int currentIndex = elementOrder.indexOf(element.getName());
if (currentIndex != -1 && currentIndex <= targetIndex) {
return i + 1;
}
}
}
return 0;
}
/**
* Checks if there should be a blank line between two elements.
* This method determines spacing based on the element order configuration.
* Empty strings in the element order indicate where blank lines should be placed.
*/
private static boolean isBlankLineBetweenElements(
String prependingElementName, String elementName, Element parent) {
List<String> elementOrder = ELEMENT_ORDER.get(parent.getName());
if (elementOrder == null || elementOrder.isEmpty()) {
return false;
}
int prependingIndex = elementOrder.indexOf(prependingElementName);
int currentIndex = elementOrder.indexOf(elementName);
if (prependingIndex == -1 || currentIndex == -1) {
return false;
}
// Check if there's an empty string between the two elements in the order
for (int i = prependingIndex + 1; i < currentIndex; i++) {
if (elementOrder.get(i).isEmpty()) {
return true;
}
}
return false;
}
}