From ddc160edf0169a9248c6eae8845f0956408c2f5d Mon Sep 17 00:00:00 2001 From: Mykola Dzyuba <7852483+mdzyuba@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:52:47 -0800 Subject: [PATCH] Update dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dependencies updated: - Java: 8 → 11 - JDOM: 1.1.3 → JDOM2 2.0.6.1 - Commons CLI: 1.4 → 1.9.0 - Commons Lang: 3.12.0 → 3.17.0 Plugins updated: - Maven Assembly: 3.1.1 → 3.7.1 - Maven Surefire: 3.2.5 → 3.5.2 Java 11 requirement: The following dependencies require Java 11 as minimum: - JUnit Jupiter 5.11.4 (requires Java 11+) - Spotless Maven Plugin 2.44.0 (requires Java 11+) Documentation: - Updated README Code changes: - Refactored AndroidXmlOutputter to use composition instead of inheritance since JDOM2's XMLOutputter cannot be subclassed. --- README.md | 2 +- pom.xml | 24 +- .../AndroidXmlOutputter.java | 585 ++++++++---------- .../bytehamster/androidxmlformatter/Main.java | 11 +- .../androidxmlformatter/IntegrationTest.java | 4 +- 5 files changed, 295 insertions(+), 331 deletions(-) diff --git a/README.md b/README.md index 5a60f67..0bc036d 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Formats XML files according to Android Studio's default formatting rules. By def ## Requirements -- Java 8 or higher +- Java 11 or higher - Maven 3.6 or higher ## Building diff --git a/pom.xml b/pom.xml index 21b0b71..5a73b50 100644 --- a/pom.xml +++ b/pom.xml @@ -9,30 +9,31 @@ 1.0-SNAPSHOT - 8 - 8 + 11 + 11 + UTF-8 commons-cli commons-cli - 1.4 + 1.9.0 org.apache.commons commons-lang3 - 3.12.0 + 3.17.0 org.jdom - jdom - 1.1.3 + jdom2 + 2.0.6.1 org.junit.jupiter junit-jupiter - 5.10.2 + 5.11.4 test @@ -42,7 +43,7 @@ com.diffplug.spotless spotless-maven-plugin - 2.43.0 + 2.44.0 @@ -67,7 +68,7 @@ maven-assembly-plugin - 3.1.1 + 3.7.1 jar-with-dependencies @@ -89,6 +90,11 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + diff --git a/src/main/java/com/bytehamster/androidxmlformatter/AndroidXmlOutputter.java b/src/main/java/com/bytehamster/androidxmlformatter/AndroidXmlOutputter.java index 1d2ad14..f56ae39 100644 --- a/src/main/java/com/bytehamster/androidxmlformatter/AndroidXmlOutputter.java +++ b/src/main/java/com/bytehamster/androidxmlformatter/AndroidXmlOutputter.java @@ -1,25 +1,30 @@ package com.bytehamster.androidxmlformatter; import org.apache.commons.lang3.StringUtils; -import org.jdom.Attribute; -import org.jdom.CDATA; -import org.jdom.Comment; -import org.jdom.Element; -import org.jdom.EntityRef; -import org.jdom.Namespace; -import org.jdom.ProcessingInstruction; -import org.jdom.Text; -import org.jdom.Verifier; -import org.jdom.output.Format; -import org.jdom.output.XMLOutputter; +import org.jdom2.Attribute; +import org.jdom2.CDATA; +import org.jdom2.Comment; +import org.jdom2.Content; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.EntityRef; +import org.jdom2.Namespace; +import org.jdom2.ProcessingInstruction; +import org.jdom2.Text; +import org.jdom2.Verifier; +import org.jdom2.output.Format; +import org.jdom2.output.XMLOutputter; import java.io.IOException; +import java.io.OutputStream; import java.io.Writer; import java.util.ArrayList; import java.util.Collections; import java.util.List; -public class AndroidXmlOutputter extends XMLOutputter { +public class AndroidXmlOutputter { + private final XMLOutputter outputter; + private final Format format; final String[] namespaceOrder; final String[] attributeNameOrder; final int attributeIndention; @@ -35,392 +40,346 @@ public AndroidXmlOutputter(int indention, int attributeIndention, this.alphabeticalAttributes = alphabeticalAttributes; this.alphabeticalNamespaces = alphabeticalNamespaces; - Format format = Format.getPrettyFormat(); - format.setIndent(StringUtils.repeat(" ", indention)); - format.setLineSeparator("\n"); - format.setEncoding("utf-8"); - setFormat(format); - } + this.format = Format.getPrettyFormat(); + this.format.setIndent(StringUtils.repeat(" ", indention)); + this.format.setLineSeparator("\n"); + this.format.setEncoding("utf-8"); - static private int elementDepth(Element element) { - int result = 0; - while (element != null) { - result++; - element = element.getParentElement(); - } - return result; + this.outputter = new XMLOutputter(this.format); } - private void printNamespace(Writer out, Namespace ns, XMLOutputter.NamespaceStack namespaces) - throws IOException { - String prefix = ns.getPrefix(); - String uri = ns.getURI(); - if (!uri.equals(namespaces.getURI(prefix))) { - out.write("xmlns"); - if (!prefix.equals("")) { - out.write(":"); - out.write(prefix); - } + public Format getFormat() { + return format; + } - out.write("=\""); - out.write(this.escapeAttributeEntities(uri)); - out.write("\""); - namespaces.push(ns); - } + public void output(Document doc, Writer writer) throws IOException { + writer.write("\n"); + Element root = doc.getRootElement(); + printElement(writer, root, 0); } - private void printElementNamespace(Writer out, Element element, - XMLOutputter.NamespaceStack namespaces) throws IOException { - Namespace ns = element.getNamespace(); - if (ns != Namespace.XML_NAMESPACE) { - if (ns != Namespace.NO_NAMESPACE || namespaces.getURI("") != null) { - this.printNamespace(out, ns, namespaces); - } + public void output(Document doc, OutputStream stream) throws IOException { + stream.write("\n".getBytes()); + Element root = doc.getRootElement(); + StringBuilder sb = new StringBuilder(); + printElementToBuilder(sb, root, 0); + stream.write(sb.toString().getBytes()); + } - } + private void printElement(Writer writer, Element element, int depth) throws IOException { + StringBuilder sb = new StringBuilder(); + printElementToBuilder(sb, element, depth); + writer.write(sb.toString()); } - private void printAdditionalNamespaces(Writer out, Element element, - XMLOutputter.NamespaceStack namespaces) throws IOException { - List list = element.getAdditionalNamespaces(); - if (list != null) { - for (int i = 0; i < list.size(); ++i) { - Namespace additional = (Namespace) list.get(i); - if (attributeIndention > 0) { - newline(out); - indent(out, elementDepth(element) - 1); - out.write(StringUtils.repeat(" ", attributeIndention)); - } else { - out.write(" "); - } - this.printNamespace(out, additional, namespaces); - } - } + private void printElementToBuilder(StringBuilder sb, Element element, int depth) { + String indent = StringUtils.repeat(format.getIndent(), depth); - } + sb.append(indent); + sb.append("<"); + printQualifiedName(sb, element); - private int skipTrailingWhite(List content, int start) { - int size = content.size(); - if (start > size) { - start = size; + // Print namespace declarations for this element's namespace if at root + if (depth == 0 && !element.getNamespace().equals(Namespace.NO_NAMESPACE)) { + sb.append(" "); + printNamespaceDeclaration(sb, element.getNamespace()); } - int index = start; - if (this.currentFormat.getTextMode() == Format.TextMode.TRIM_FULL_WHITE - || this.currentFormat.getTextMode() == Format.TextMode.NORMALIZE - || this.currentFormat.getTextMode() == Format.TextMode.TRIM) { - while (index >= 0 && this.isAllWhitespace(content.get(index - 1))) { - --index; + // Print additional namespace declarations + for (Namespace ns : element.getAdditionalNamespaces()) { + if (attributeIndention > 0) { + sb.append("\n"); + sb.append(indent); + sb.append(StringUtils.repeat(" ", attributeIndention)); + } else { + sb.append(" "); } + printNamespaceDeclaration(sb, ns); } - return index; - } + // Print attributes + List attributes = element.getAttributes(); + if (attributes != null && !attributes.isEmpty()) { + printAttributes(sb, element, depth); + } - private boolean isAllWhitespace(Object obj) { - String str = null; - if (obj instanceof String) { - str = (String) obj; + List content = element.getContent(); + int start = skipLeadingWhite(content, 0); + int size = content.size(); + + if (start >= size) { + sb.append(" />\n"); } else { - if (!(obj instanceof Text)) { - if (obj instanceof EntityRef) { - return false; + sb.append(">"); + if (nextNonText(content, start) < size) { + sb.append("\n"); + if (hasBlankLine(content, 0, start)) { + sb.append("\n"); } - - return false; + printContentRange(sb, content, start, size, depth + 1); + sb.append(indent); + } else { + printTextRange(sb, content, start, size); } - - str = ((Text) obj).getText(); + sb.append("\n"); } + } - for (int i = 0; i < str.length(); ++i) { - if (!Verifier.isXMLWhitespace(str.charAt(i))) { - return false; - } + private void printNamespaceDeclaration(StringBuilder sb, Namespace ns) { + sb.append("xmlns"); + if (!ns.getPrefix().isEmpty()) { + sb.append(":"); + sb.append(ns.getPrefix()); } - - return true; + sb.append("=\""); + sb.append(escapeAttribute(ns.getURI())); + sb.append("\""); } - private int skipLeadingWhite(List content, int start) { - if (start < 0) { - start = 0; - } + private void printAttributes(StringBuilder sb, Element parent, int depth) { + List attributes = new ArrayList<>(parent.getAttributes()); + String indent = StringUtils.repeat(format.getIndent(), depth); - int index = start; - int size = content.size(); - if (true) { - while (index < size) { - if (!this.isAllWhitespace(content.get(index))) { - return index; + Collections.sort(attributes, (a1, a2) -> { + if (!a1.getNamespacePrefix().equals(a2.getNamespacePrefix())) { + for (String namespace : namespaceOrder) { + if (a1.getNamespacePrefix().equals(namespace)) { + return -1; + } else if (a2.getNamespacePrefix().equals(namespace)) { + return 1; + } + } + if (alphabeticalNamespaces) { + return a1.getNamespacePrefix().compareTo(a2.getNamespacePrefix()); } - - ++index; } - } + for (String name : attributeNameOrder) { + if (a1.getName().equals(name)) { + return -1; + } else if (a2.getName().equals(name)) { + return 1; + } + } + if (alphabeticalAttributes) { + return a1.getName().compareTo(a2.getName()); + } else { + return 0; // Sort is stable + } + }); - return index; - } + for (Attribute attrib : attributes) { + if (attributeIndention > 0) { + sb.append("\n"); + sb.append(indent); + sb.append(StringUtils.repeat(" ", attributeIndention)); + } else { + sb.append(" "); + } - private static int nextNonText(List content, int start) { - if (start < 0) { - start = 0; + printQualifiedName(sb, attrib); + sb.append("=\""); + sb.append(escapeAttribute(attrib.getValue())); + sb.append("\""); } + } + private void printContentRange( + StringBuilder sb, List content, int start, int end, int depth) { int index = start; - int size; - for (size = content.size(); index < size; ++index) { - Object node = content.get(index); - if (!(node instanceof Text) && !(node instanceof EntityRef)) { - return index; + while (index < end) { + Content next = content.get(index); + if (!(next instanceof Text) && !(next instanceof EntityRef)) { + String indent = StringUtils.repeat(format.getIndent(), depth); + if (next instanceof Comment) { + sb.append(indent); + sb.append("\n"); + } else if (next instanceof Element) { + printElementToBuilder(sb, (Element) next, depth); + } else if (next instanceof ProcessingInstruction) { + ProcessingInstruction pi = (ProcessingInstruction) next; + sb.append(indent); + sb.append("\n"); + } else if (next instanceof CDATA) { + sb.append(indent); + sb.append("\n"); + } + ++index; + } else { + int originalIndex = index; + int first = skipLeadingWhite(content, index); + index = nextNonText(content, first); + if (first < index) { + String indent = StringUtils.repeat(format.getIndent(), depth); + sb.append(indent); + printTextRange(sb, content, first, index); + sb.append("\n"); + } else if (hasBlankLine(content, originalIndex, first)) { + sb.append("\n"); + } } } - - return size; } - private void printContentRange(Writer out, List content, int start, int end, int level, - XMLOutputter.NamespaceStack namespaces) throws IOException { - int index = start; - - while (true) { - while (index < end) { - boolean firstNode = index == start; - Object next = content.get(index); - if (!(next instanceof Text) && !(next instanceof EntityRef)) { - if (!firstNode) { - this.newline(out); - } - - this.indent(out, level); - if (next instanceof Comment) { - this.printComment(out, (Comment) next); - } else if (next instanceof Element) { - this.printElement(out, (Element) next, level, namespaces); - } else if (next instanceof ProcessingInstruction) { - this.printProcessingInstruction(out, (ProcessingInstruction) next); - } - - ++index; - } else { - int first = this.skipLeadingWhite(content, index); - index = nextNonText(content, first); - if (first < index) { - if (!firstNode) { - this.newline(out); + private boolean hasBlankLine(List content, int start, int end) { + for (int i = start; i < end; i++) { + Content node = content.get(i); + if (node instanceof Text) { + String text = ((Text) node).getText(); + int newlineCount = 0; + for (int j = 0; j < text.length(); j++) { + if (text.charAt(j) == '\n') { + newlineCount++; + if (newlineCount >= 2) { + return true; } - - this.indent(out, level); - this.printTextRange(out, content, first, index); } } } - - return; } + return false; } - private void printString(Writer out, String str) throws IOException { - if (this.currentFormat.getTextMode() == Format.TextMode.NORMALIZE) { - str = Text.normalizeString(str); - } else if (this.currentFormat.getTextMode() == Format.TextMode.TRIM) { - str = str.trim(); - } - - out.write(this.escapeElementEntities(str)); - } - - private void printTextRange(Writer out, List content, int start, int end) throws IOException { - String previous = null; - start = this.skipLeadingWhite(content, start); + private void printTextRange(StringBuilder sb, List content, int start, int end) { + start = skipLeadingWhite(content, start); int size = content.size(); if (start < size) { - end = this.skipTrailingWhite(content, end); + end = skipTrailingWhite(content, end); for (int i = start; i < end; ++i) { - Object node = content.get(i); - String next; + Content node = content.get(i); + String text; if (node instanceof Text) { - next = ((Text) node).getText(); + text = ((Text) node).getText().trim(); + } else if (node instanceof EntityRef) { + text = "&" + ((EntityRef) node).getName() + ";"; + } else if (node instanceof CDATA) { + sb.append(""); + continue; } else { - if (!(node instanceof EntityRef)) { - throw new IllegalStateException( - "Should see only CDATA, Text, or EntityRef"); - } - - next = "&" + ((EntityRef) node).getValue() + ";"; + continue; } - if (next != null && !"".equals(next)) { - if (previous != null - && (this.currentFormat.getTextMode() == Format.TextMode.NORMALIZE - || this.currentFormat.getTextMode() == Format.TextMode.TRIM) - && (this.endsWithWhite(previous) || this.startsWithWhite(next))) { - out.write(" "); - } - - if (node instanceof CDATA) { - this.printCDATA(out, (CDATA) node); - } else if (node instanceof EntityRef) { - this.printEntityRef(out, (EntityRef) node); - } else { - this.printString(out, next); - } - - previous = next; + if (text != null && !text.isEmpty()) { + sb.append(escapeText(text)); } } } - - } - - private boolean startsWithWhite(String str) { - return str != null && str.length() > 0 && Verifier.isXMLWhitespace(str.charAt(0)); } - private boolean endsWithWhite(String str) { - return str != null && str.length() > 0 - && Verifier.isXMLWhitespace(str.charAt(str.length() - 1)); - } - - @Override - protected void printElement(Writer out, Element element, int level, NamespaceStack namespaces) - throws IOException { - List attributes = element.getAttributes(); - List content = element.getContent(); - String space = null; - if (attributes != null) { - space = element.getAttributeValue("space", Namespace.XML_NAMESPACE); - } - - Format previousFormat = this.currentFormat; - if ("default".equals(space)) { - this.currentFormat = this.getFormat(); - } else if ("preserve".equals(space)) { - this.currentFormat = preserveFormat; - } - - out.write("<"); - this.printQualifiedName(out, element); - int previouslyDeclaredNamespaces = namespaces.size(); - this.printElementNamespace(out, element, namespaces); - this.printAdditionalNamespaces(out, element, namespaces); - if (attributes != null) { - this.printAttributes(out, attributes, element, namespaces); + private int skipLeadingWhite(List content, int start) { + if (start < 0) { + start = 0; } - int start = this.skipLeadingWhite(content, 0); + int index = start; int size = content.size(); - if (start >= size) { - out.write(" />"); - } else { - out.write(">"); - newline(out); - if (nextNonText(content, start) < size) { - this.newline(out); - this.printContentRange(out, content, start, size, level + 1, namespaces); - this.newline(out); - this.indent(out, level); - } else { - this.printTextRange(out, content, start, size); + while (index < size) { + if (!isAllWhitespace(content.get(index))) { + return index; } + ++index; + } + return index; + } - out.write(""); + private int skipTrailingWhite(List content, int start) { + int size = content.size(); + if (start > size) { + start = size; } - while (namespaces.size() > previouslyDeclaredNamespaces) { - namespaces.pop(); + int index = start; + while (index > 0 && isAllWhitespace(content.get(index - 1))) { + --index; } - this.currentFormat = previousFormat; - newline(out); + return index; } - private void newline(Writer out) throws IOException { - out.write(getFormat().getLineSeparator()); - } + private boolean isAllWhitespace(Object obj) { + String str = null; + if (obj instanceof String) { + str = (String) obj; + } else if (obj instanceof Text) { + str = ((Text) obj).getText(); + } else if (obj instanceof EntityRef) { + return false; + } else { + return false; + } - private void indent(Writer out, int level) throws IOException { - for (int i = 0; i < level; ++i) { - out.write(getFormat().getIndent()); + for (int i = 0; i < str.length(); ++i) { + if (!Verifier.isXMLWhitespace(str.charAt(i))) { + return false; + } } + + return true; } - @Override - protected void printAttributes(Writer writer, List attribs, Element parent, NamespaceStack ns) - throws IOException { - List attributes = new ArrayList<>(); - for (Object attribObj : attribs) { - attributes.add((Attribute) attribObj); + private int nextNonText(List content, int start) { + if (start < 0) { + start = 0; } - Collections.sort(attributes, (a1, a2) -> { - if (!a1.getNamespacePrefix().equals(a2.getNamespacePrefix())) { - for (String namespace : namespaceOrder) { - if (a1.getNamespacePrefix().equals(namespace)) { - return -1; - } else if (a2.getNamespacePrefix().equals(namespace)) { - return 1; - } - } - if (alphabeticalNamespaces) { - return a1.getNamespacePrefix().compareTo(a2.getNamespacePrefix()); - } - } - for (String name : attributeNameOrder) { - if (a1.getName().equals(name)) { - return -1; - } else if (a2.getName().equals(name)) { - return 1; - } - } - if (alphabeticalAttributes) { - return a1.getName().compareTo(a2.getName()); - } else { - return 0; // Sort is stable - } - }); - - for (Attribute attrib : attributes) { - if (attributeIndention > 0) { - newline(writer); - indent(writer, elementDepth(parent) - 1); - writer.write(StringUtils.repeat(" ", attributeIndention)); - } else { - writer.write(" "); + int index = start; + int size = content.size(); + while (index < size) { + Content node = content.get(index); + if (!(node instanceof Text) && !(node instanceof EntityRef)) { + return index; } - - printQualifiedName(writer, attrib); - writer.write("="); - writer.write("\""); - writer.write(escapeAttributeEntities(attrib.getValue())); - writer.write("\""); + ++index; } + + return size; } - private void printQualifiedName(Writer out, Attribute a) throws IOException { + private void printQualifiedName(StringBuilder sb, Attribute a) { String prefix = a.getNamespace().getPrefix(); - if (prefix != null && !prefix.equals("")) { - out.write(prefix); - out.write(58); - out.write(a.getName()); - } else { - out.write(a.getName()); + if (prefix != null && !prefix.isEmpty()) { + sb.append(prefix); + sb.append(":"); } + sb.append(a.getName()); + } + private void printQualifiedName(StringBuilder sb, Element e) { + if (!e.getNamespace().getPrefix().isEmpty()) { + sb.append(e.getNamespace().getPrefix()); + sb.append(":"); + } + sb.append(e.getName()); } - private void printQualifiedName(Writer out, Element e) throws IOException { - if (e.getNamespace().getPrefix().length() == 0) { - out.write(e.getName()); - } else { - out.write(e.getNamespace().getPrefix()); - out.write(58); - out.write(e.getName()); + private String escapeAttribute(String value) { + if (value == null) { + return ""; } + return value + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """); + } + private String escapeText(String value) { + if (value == null) { + return ""; + } + return value.replace("&", "&").replace("<", "<").replace(">", ">"); } } diff --git a/src/main/java/com/bytehamster/androidxmlformatter/Main.java b/src/main/java/com/bytehamster/androidxmlformatter/Main.java index 9d4cbad..18b77c1 100644 --- a/src/main/java/com/bytehamster/androidxmlformatter/Main.java +++ b/src/main/java/com/bytehamster/androidxmlformatter/Main.java @@ -6,9 +6,8 @@ import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; -import org.jdom.Document; -import org.jdom.input.SAXBuilder; -import org.jdom.output.XMLOutputter; +import org.jdom2.Document; +import org.jdom2.input.SAXBuilder; import java.io.ByteArrayOutputStream; import java.io.File; @@ -59,7 +58,7 @@ public static void main(String[] args) throws Exception { for (String filename : cmd.getArgList()) { Document doc = new SAXBuilder().build(new FileInputStream(filename)); - XMLOutputter outputter = new AndroidXmlOutputter( + AndroidXmlOutputter outputter = new AndroidXmlOutputter( Integer.parseInt(cmd.getOptionValue("indention", "4")), Integer.parseInt(cmd.getOptionValue("attribute-indention", "4")), cmd.getOptionValue("namespace-order", "android").split(","), @@ -70,8 +69,8 @@ public static void main(String[] args) throws Exception { ByteArrayOutputStream stream = new ByteArrayOutputStream(); outputter.output(doc, stream); byte[] content = stream.toByteArray(); - new FileOutputStream(filename).write(content, 0, content.length - 2); // Strip double - // line break + new FileOutputStream(filename) + .write(content, 0, content.length - 2); // Strip double line break } } } diff --git a/src/test/java/com/bytehamster/androidxmlformatter/IntegrationTest.java b/src/test/java/com/bytehamster/androidxmlformatter/IntegrationTest.java index 7359f0a..3833d5d 100644 --- a/src/test/java/com/bytehamster/androidxmlformatter/IntegrationTest.java +++ b/src/test/java/com/bytehamster/androidxmlformatter/IntegrationTest.java @@ -2,8 +2,8 @@ import static org.junit.jupiter.api.Assertions.*; -import org.jdom.Document; -import org.jdom.input.SAXBuilder; +import org.jdom2.Document; +import org.jdom2.input.SAXBuilder; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test;