org.openrewrite.xml.tree.Xml Maven / Gradle / Ivy
Show all versions of rewrite-xml Show documentation
/*
* Copyright 2020 the original author or authors.
*
* Licensed 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
*
* https://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.openrewrite.xml.tree;
import lombok.*;
import lombok.experimental.FieldDefaults;
import org.apache.commons.text.StringEscapeUtils;
import org.intellij.lang.annotations.Language;
import org.jspecify.annotations.Nullable;
import org.openrewrite.*;
import org.openrewrite.internal.WhitespaceValidationService;
import org.openrewrite.marker.Markers;
import org.openrewrite.xml.XmlParser;
import org.openrewrite.xml.XmlVisitor;
import org.openrewrite.xml.internal.WithPrefix;
import org.openrewrite.xml.internal.XmlPrinter;
import org.openrewrite.xml.internal.XmlWhitespaceValidationService;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.toList;
import static org.openrewrite.Tree.randomId;
/**
* The XML spec.
*/
public interface Xml extends Tree {
@SuppressWarnings("unchecked")
@Override
default R accept(TreeVisitor v, P p) {
return (R) acceptXml(v.adapt(XmlVisitor.class), p);
}
default @Nullable Xml acceptXml(XmlVisitor
v, P p) {
return v.defaultValue(this, p);
}
@Override
default
boolean isAcceptable(TreeVisitor, P> v, P p) {
return v.isAdaptableTo(XmlVisitor.class);
}
String getPrefix();
Xml withPrefix(String prefix);
/**
* @param prefix The new prefix
* @return An XML AST with the new prefix set, even if the old and new prefix pass a
* string equality check. The receiver is unchanged if the old and new prefix pass a
* referential equality check.
*/
Xml withPrefixUnsafe(String prefix);
@Getter
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true)
@RequiredArgsConstructor
class Document implements Xml, SourceFile {
@With
@EqualsAndHashCode.Include
UUID id;
@With
Path sourcePath;
@With
String prefixUnsafe;
@Override
public Document withPrefix(String prefix) {
return WithPrefix.onlyIfNotEqual(this, prefix);
}
@Override
public String getPrefix() {
return prefixUnsafe;
}
@With
Markers markers;
@Nullable // for backwards compatibility
@With(AccessLevel.PRIVATE)
String charsetName;
@With
boolean charsetBomMarked;
@With
@Nullable
Checksum checksum;
@With
@Nullable
FileAttributes fileAttributes;
@Override
public Charset getCharset() {
return charsetName == null ? StandardCharsets.UTF_8 : Charset.forName(charsetName);
}
@SuppressWarnings("unchecked")
@Override
public Xml.Document withCharset(Charset charset) {
return withCharsetName(charset.name());
}
@With
Prolog prolog;
@With
Tag root;
String eof;
public Document withEof(String eof) {
if (this.eof.equals(eof)) {
return this;
}
return new Document(id, sourcePath, prefixUnsafe, markers, charsetName, charsetBomMarked, checksum, fileAttributes, prolog, root, eof);
}
@Override
public
Xml acceptXml(XmlVisitor
v, P p) {
return v.visitDocument(this, p);
}
@Override
public
TreeVisitor, PrintOutputCapture
> printer(Cursor cursor) {
return new XmlPrinter<>();
}
@SuppressWarnings("unchecked")
@Override
public T service(Class service) {
if (WhitespaceValidationService.class.getName().equals(service.getName())) {
return (T) new XmlWhitespaceValidationService();
}
return SourceFile.super.service(service);
}
}
@Value
@EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true)
@With
class Prolog implements Xml {
@EqualsAndHashCode.Include
UUID id;
String prefixUnsafe;
@Override
public Prolog withPrefix(String prefix) {
return WithPrefix.onlyIfNotEqual(this, prefix);
}
@Override
public String getPrefix() {
return prefixUnsafe;
}
Markers markers;
@Nullable
XmlDecl xmlDecl;
List misc;
List jspDirectives;
@Override
public Xml acceptXml(XmlVisitor
v, P p) {
return v.visitProlog(this, p);
}
}
@Value
@EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true)
@With
class XmlDecl implements Xml, Misc {
@EqualsAndHashCode.Include
UUID id;
String prefixUnsafe;
@Override
public XmlDecl withPrefix(String prefix) {
return WithPrefix.onlyIfNotEqual(this, prefix);
}
@Override
public String getPrefix() {
return prefixUnsafe;
}
Markers markers;
String name;
List attributes;
/**
* Space before '>'
*/
String beforeTagDelimiterPrefix;
@Override
public Xml acceptXml(XmlVisitor
v, P p) {
return v.visitXmlDecl(this, p);
}
}
@Value
@EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true)
@With
class ProcessingInstruction implements Xml, Content, Misc {
@EqualsAndHashCode.Include
UUID id;
String prefixUnsafe;
@Override
public ProcessingInstruction withPrefix(String prefix) {
return WithPrefix.onlyIfNotEqual(this, prefix);
}
@Override
public String getPrefix() {
return prefixUnsafe;
}
Markers markers;
String name;
CharData processingInstructions;
/**
* Space before '>'
*/
String beforeTagDelimiterPrefix;
@Override
public
Xml acceptXml(XmlVisitor
v, P p) {
return v.visitProcessingInstruction(this, p);
}
}
@SuppressWarnings("unused")
@Value
@EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true)
class Tag implements Xml, Content {
@EqualsAndHashCode.Include
@With
UUID id;
@With
String prefixUnsafe;
@Override
public Tag withPrefix(String prefix) {
return WithPrefix.onlyIfNotEqual(this, prefix);
}
@Override
public String getPrefix() {
return prefixUnsafe;
}
@With
Markers markers;
/**
* XML does not allow space between the '<' and tag name.
*/
String name;
public static Xml.Tag build(@Language("xml") String tagSource) {
return new XmlParser().parse(tagSource)
.findFirst()
.map(Xml.Document.class::cast)
.orElseThrow(() -> new IllegalArgumentException("Could not parse as XML"))
.getRoot();
}
public Tag withName(String name) {
if(!name.equals(name.trim())) {
throw new IllegalArgumentException("Tag name must not contain leading or trailing whitespace");
}
if(this.name.equals(name)) {
return this;
}
return new Tag(id, prefixUnsafe, markers, name, attributes, content,
closing == null ? null : closing.withName(name),
beforeTagDelimiterPrefix);
}
public Tag withValue(String value) {
CharData charData;
if (content != null && content.get(0) instanceof CharData) {
charData = ((CharData) content.get(0)).withText(value);
} else {
charData = new CharData(randomId(), "", Markers.EMPTY,
false, value, "");
}
return withContent(Collections.singletonList(charData));
}
@With
List attributes;
@Nullable
List extends Content> content;
public Optional getChild(String name) {
return content == null ? Optional.empty() : content.stream()
.filter(t -> t instanceof Xml.Tag)
.map(Tag.class::cast)
.filter(t -> t.getName().equals(name))
.findAny();
}
public List getChildren(String name) {
return content == null ? emptyList() : content.stream()
.filter(t -> t instanceof Xml.Tag)
.map(Tag.class::cast)
.filter(t -> t.getName().equals(name))
.collect(toList());
}
public List getChildren() {
return content == null ? emptyList() : content.stream()
.filter(t -> t instanceof Xml.Tag)
.map(Tag.class::cast)
.collect(toList());
}
/**
* Locate a child tag with the given name and set its text value.
*
* @param childName The child tag to locate. This assumes there is one and only one.
* @param text The text value to set.
* @return This tag.
*/
public Xml.Tag withChildValue(String childName, String text) {
return getChild(childName)
.map(tag -> this.withContent(
content == null ?
null :
content.stream()
.map(content -> content == tag ?
((Tag) content).withValue(text) :
content)
.collect(toList())
))
.orElse(this);
}
/**
* @return If this tag's content is only character data, consider it the value.
*/
public Optional getValue() {
if (content == null) {
return Optional.empty();
}
if (content.size() == 1 && content.get(0) instanceof Xml.CharData) {
return Optional.ofNullable(((CharData) content.get(0)).getText());
}
if (content.stream().allMatch(c -> c instanceof Xml.CharData)) {
return Optional.of(content.stream()
.map(c -> ((CharData) c).getText())
.map(StringEscapeUtils::unescapeXml)
.collect(Collectors.joining()));
}
return Optional.empty();
}
/**
* A shortcut for {@link #getChild(String)} and {@link #getValue()}.
*
* @param name The name of the child element to look for.
* @return The character data of the first child element matching the provided name, if any.
*/
public Optional getChildValue(String name) {
return getChild(name).flatMap(Tag::getValue);
}
public Optional getSibling(String name, Cursor cursor) {
if (cursor.getParent() == null) {
return Optional.empty();
}
Xml.Tag parent = cursor.getParent().getValue();
return parent.getChild(name);
}
public Tag withContent(@Nullable List extends Content> content) {
if (this.content == content) {
return this;
}
Tag tag = new Tag(id, prefixUnsafe, markers, name, attributes, content, closing,
beforeTagDelimiterPrefix);
if (closing == null) {
if (content != null && !content.isEmpty()) {
// TODO test this
String indentedClosingTagPrefix = prefixUnsafe.substring(Math.max(0, prefixUnsafe.lastIndexOf('\n')));
if (content.get(0) instanceof CharData) {
return tag.withClosing(new Closing(randomId(),
content.get(0).getPrefix().contains("\n") ?
indentedClosingTagPrefix : "",
Markers.EMPTY,
name, ""));
} else {
return tag.withClosing(new Closing(randomId(),
indentedClosingTagPrefix, Markers.EMPTY,
name, ""));
}
}
}
return tag;
}
@With
@Nullable
Closing closing;
/**
* Space before '>' or '/>'
*/
@With
String beforeTagDelimiterPrefix;
@Override
public Xml acceptXml(XmlVisitor
v, P p) {
return v.visitTag(this, p);
}
@Override
public String toString() {
return "<" + name + attributes.stream().map(a -> " " + a.getKey().getName() + "=\"" + a.getValueAsString() + "\"")
.collect(Collectors.joining("")) + ">";
}
@Value
@EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true)
@With
public static class Closing implements Xml {
@EqualsAndHashCode.Include
UUID id;
String prefixUnsafe;
@Override
public Closing withPrefix(String prefix) {
return WithPrefix.onlyIfNotEqual(this, prefix);
}
@Override
public String getPrefix() {
return prefixUnsafe;
}
Markers markers;
String name;
/**
* Space before '>'
*/
String beforeTagDelimiterPrefix;
@Override
public
Xml acceptXml(XmlVisitor
v, P p) {
return v.visitTagClosing(this, p);
}
@Override
public String toString() {
return "" + name + ">";
}
}
}
@lombok.Value
@EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true)
@With
class Attribute implements Xml {
@EqualsAndHashCode.Include
UUID id;
String prefixUnsafe;
@Override
public Attribute withPrefix(String prefix) {
return WithPrefix.onlyIfNotEqual(this, prefix);
}
@Override
public String getPrefix() {
return prefixUnsafe;
}
Markers markers;
Ident key;
String beforeEquals;
Value value;
@Override
public
Xml acceptXml(XmlVisitor
v, P p) {
return v.visitAttribute(this, p);
}
@lombok.Value
@EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true)
@With
public static class Value implements Xml {
public enum Quote {
Double, Single
}
@EqualsAndHashCode.Include
UUID id;
String prefixUnsafe;
@Override
public Value withPrefix(String prefix) {
return WithPrefix.onlyIfNotEqual(this, prefix);
}
@Override
public String getPrefix() {
return prefixUnsafe;
}
Markers markers;
Quote quote;
String value;
@Override
public
Xml acceptXml(XmlVisitor
v, P p) {
return v.visitAttributeValue(this, p);
}
}
public String getKeyAsString() {
return key.getName();
}
public String getValueAsString() {
return value.getValue();
}
@Override
public String toString() {
return getKeyAsString() + "=" + getValueAsString();
}
}
@Value
@EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true)
@With
class CharData implements Xml, Content {
@EqualsAndHashCode.Include
UUID id;
String prefixUnsafe;
@Override
public CharData withPrefix(String prefix) {
return WithPrefix.onlyIfNotEqual(this, prefix);
}
@Override
public String getPrefix() {
return prefixUnsafe;
}
Markers markers;
boolean cdata;
String text;
String afterText;
@Override
public
Xml acceptXml(XmlVisitor
v, P p) {
return v.visitCharData(this, p);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("text = \"").append(text).append("\"");
if (afterText != null && !afterText.isEmpty()) {
sb.append(" afterText = \"").append(afterText).append("\"");
}
return sb.toString();
}
}
@Value
@EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true)
@With
class Comment implements Xml, Content, Misc {
@EqualsAndHashCode.Include
UUID id;
String prefixUnsafe;
@Override
public Comment withPrefix(String prefix) {
return WithPrefix.onlyIfNotEqual(this, prefix);
}
@Override
public String getPrefix() {
return prefixUnsafe;
}
Markers markers;
String text;
@Override
public
Xml acceptXml(XmlVisitor
v, P p) {
return v.visitComment(this, p);
}
@Override
public String toString() {
return "";
}
}
@Value
@EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true)
@With
class DocTypeDecl implements Xml, Misc {
@EqualsAndHashCode.Include
UUID id;
String prefixUnsafe;
@Override
public DocTypeDecl withPrefix(String prefix) {
return WithPrefix.onlyIfNotEqual(this, prefix);
}
@Override
public String getPrefix() {
return prefixUnsafe;
}
Markers markers;
Ident name;
@Nullable
Ident externalId;
List internalSubset;
@Nullable
ExternalSubsets externalSubsets;
/**
* Space before '>'.
*/
String beforeTagDelimiterPrefix;
@Value
@EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true)
@With
public static class ExternalSubsets implements Xml {
@EqualsAndHashCode.Include
UUID id;
String prefixUnsafe;
@Override
public ExternalSubsets withPrefix(String prefix) {
return WithPrefix.onlyIfNotEqual(this, prefix);
}
@Override
public String getPrefix() {
return prefixUnsafe;
}
Markers markers;
List elements;
@Override
public Xml acceptXml(XmlVisitor
v, P p) {
return v.visitDocTypeDeclExternalSubsets(this, p);
}
}
@Override
public
Xml acceptXml(XmlVisitor
v, P p) {
return v.visitDocTypeDecl(this, p);
}
}
@Value
@EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true)
@With
class Element implements Xml {
@EqualsAndHashCode.Include
UUID id;
String prefixUnsafe;
@Override
public Element withPrefix(String prefix) {
return WithPrefix.onlyIfNotEqual(this, prefix);
}
@Override
public String getPrefix() {
return prefixUnsafe;
}
Markers markers;
List subset;
/**
* Space before '>'
*/
String beforeTagDelimiterPrefix;
@Override
public Xml acceptXml(XmlVisitor
v, P p) {
return v.visitElement(this, p);
}
}
@Value
@EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true)
@With
class Ident implements Xml {
@EqualsAndHashCode.Include
UUID id;
String prefixUnsafe;
@Override
public Ident withPrefix(String prefix) {
return WithPrefix.onlyIfNotEqual(this, prefix);
}
@Override
public String getPrefix() {
return prefixUnsafe;
}
Markers markers;
String name;
@Override
public
Xml acceptXml(XmlVisitor
v, P p) {
return v.visitIdent(this, p);
}
@Override
public String toString() {
return "Ident{" + name + "}";
}
}
@Value
@EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true)
class JspDirective implements Xml, Content {
@EqualsAndHashCode.Include
@With
UUID id;
@With
String prefixUnsafe;
@Override
public JspDirective withPrefix(String prefix) {
return WithPrefix.onlyIfNotEqual(this, prefix);
}
@Override
public String getPrefix() {
return prefixUnsafe;
}
@With
Markers markers;
@With
String beforeTypePrefix;
String type;
public JspDirective withType(String type) {
return new JspDirective(id, prefixUnsafe, markers, beforeTypePrefix, type, attributes,
beforeDirectiveEndPrefix);
}
@With
List attributes;
/**
* Space before '%>'
*/
@With
String beforeDirectiveEndPrefix;
@Override
public Xml acceptXml(XmlVisitor
v, P p) {
return v.visitJspDirective(this, p);
}
@Override
public String toString() {
return "<%@ " + type + attributes.stream().map(a -> " " + a.getKey().getName() + "=\"" + a.getValueAsString() + "\"")
.collect(Collectors.joining("")) + "%>";
}
}
}