Skip to content

Instantly share code, notes, and snippets.

@robsonkades
Last active April 23, 2026 16:15
Show Gist options
  • Select an option

  • Save robsonkades/203d1c4dc1fa6a15c58c69c2d277bd68 to your computer and use it in GitHub Desktop.

Select an option

Save robsonkades/203d1c4dc1fa6a15c58c69c2d277bd68 to your computer and use it in GitHub Desktop.
https://medium.com/javarevisited/one-endpoint-multiple-payloads-handling-polymorphic-request-bodies-in-spring-boot-4e354b72450c
https://medium.com/@khanjani.hamid/advanced-jvm-heap-tuning-and-kubernetes-optimization-for-high-performance-java-applications-998b6604c70a
import com.ctc.wstx.api.WstxInputProperties;
import com.ctc.wstx.stax.WstxInputFactory;
import com.ctc.wstx.stax.WstxOutputFactory;
import com.fleekdocs.infrastructure.exception.XmlException;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Function;
/**
* Safe StAX-based XML helper for lightweight queries and structural edits.
* <p>
* Prefer {@link #document(String)} and {@link XmlDocument#select(String...)}
* for new code. The extract* methods are kept as compatibility shortcuts.
* This utility intentionally avoids DOM loading and disables DTD/external
* entity support in the underlying Woodstox parser.
*/
public final class XmlUtil {
private static final int MAX_TEXT_LENGTH = 1_000_000;
private static final ThreadLocal<WstxInputFactory> INPUT_FACTORY = ThreadLocal.withInitial(() -> {
WstxInputFactory factory = new WstxInputFactory();
factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
factory.setProperty(WstxInputProperties.P_MAX_TEXT_LENGTH, MAX_TEXT_LENGTH);
factory.setProperty(WstxInputProperties.P_MAX_ELEMENT_DEPTH, 500);
return factory;
});
private static final ThreadLocal<WstxOutputFactory> OUTPUT_FACTORY = ThreadLocal.withInitial(WstxOutputFactory::new);
private XmlUtil() {
}
/**
* Produces a compact XML string while preserving structural content such as
* attributes, namespaces, comments, processing instructions, and CDATA.
* <p>
* Blank input is returned unchanged. Malformed XML or parser limit
* violations are reported as {@link XmlException}.
*
* @param xml XML payload to compact
* @return compact XML, or the original value when the input is {@code null}
* or blank
*/
public static String minify(final String xml) {
if (xml == null || xml.isBlank()) return xml;
return withReader(xml, reader -> {
final var sw = new StringWriter(xml.length());
final var writer = OUTPUT_FACTORY.get().createXMLStreamWriter(sw);
try {
while (reader.hasNext()) {
int event = reader.next();
copyEvent(reader, writer, event);
}
writer.flush();
return sw.toString();
} finally {
writer.close();
}
});
}
/**
* Compatibility factory for the previous fluent facade.
* <p>
* New code should prefer {@link #document(String)}, which exposes the same
* query/edit operations under a clearer library-oriented name.
*
* @param xml XML payload to query or edit
* @return immutable XML facade
*/
public static FluentXml from(final String xml) {
return new FluentXml(xml);
}
/**
* Creates an immutable query facade over an XML payload.
*
* @param xml XML payload to query or edit
* @return immutable XML document facade
*/
public static XmlDocument document(final String xml) {
return new XmlDocument(xml);
}
/**
* Creates an immutable editor for structural XML insertions.
*
* @param xml source XML payload
* @return immutable editor facade
*/
public static XmlEditor edit(final String xml) {
return XmlEditor.from(xml);
}
/**
* Selects nodes using descendant path semantics.
* <p>
* Descendant selection allows unrelated intermediate elements between the
* provided segments, which preserves the legacy extract* behavior.
*
* @param xml XML payload to query
* @param path element local-name segments
* @return lazy materialization facade for the selected nodes
*/
public static XmlSelection select(final String xml, final String... path) {
return select(xml, XmlPath.descendant(path));
}
/**
* Selects XML nodes using an {@link XmlPath}. The returned selection can be
* materialized as text, XML fragments, attributes, or typed values.
*
* @param xml XML payload to query
* @param path query path
* @return lazy materialization facade for the selected nodes
*/
public static XmlSelection select(final String xml, final XmlPath path) {
return new XmlSelection(xml, path, null);
}
/**
* Extracts the text value of the first matching element using descendant
* path semantics.
*
* @param xml XML payload to query
* @param path element local-name segments
* @return selected text, or {@link Optional#empty()} when no element matches
*/
public static Optional<String> extractValue(final String xml, final String... path) {
if (xml == null || xml.isBlank()) return Optional.empty();
return extractValue(xml, XmlPath.descendant(path));
}
/**
* Extracts the text value of the first matching element.
*
* @param xml XML payload to query
* @param path query path
* @return selected text, or {@link Optional#empty()} when no element matches
*/
public static Optional<String> extractValue(final String xml, final XmlPath path) {
if (xml == null || xml.isBlank()) return Optional.empty();
final var safePath = Objects.requireNonNull(path, "path nao pode ser nulo");
return withReader(xml, reader -> {
if (navigateToPath(reader, safePath)) {
return Optional.of(extractText(reader));
}
return Optional.empty();
});
}
/**
* Extracts text values from all matching elements using descendant path
* semantics.
*
* @param xml XML payload to query
* @param path element local-name segments
* @return immutable list of selected text values
*/
public static List<String> extractValues(final String xml, final String... path) {
if (xml == null || xml.isBlank()) return List.of();
return extractValues(xml, XmlPath.descendant(path));
}
/**
* Extracts text values from matching elements.
*
* @param xml XML payload to query
* @param path query path
* @return immutable list of selected text values
*/
public static List<String> extractValues(final String xml, final XmlPath path) {
if (xml == null || xml.isBlank()) return List.of();
final var safePath = Objects.requireNonNull(path, "path nao pode ser nulo");
return withReader(xml, reader -> {
final var values = new ArrayList<String>();
collectPathMatches(reader, safePath, matchedReader -> values.add(extractText(matchedReader)));
return List.copyOf(values);
});
}
/**
* Extracts the attribute value of the first matching element using
* descendant path semantics.
*
* @param xml XML payload to query
* @param attributeName attribute local name
* @param path element local-name segments
* @return selected attribute value, or {@link Optional#empty()} when absent
*/
public static Optional<String> extractAttribute(final String xml, final String attributeName, final String... path) {
if (xml == null || xml.isBlank()) return Optional.empty();
final var safePath = XmlPath.descendant(path);
return extractAttribute(xml, attributeName, safePath);
}
/**
* Extracts the attribute value of the first matching element.
*
* @param xml XML payload to query
* @param attributeName attribute local name
* @param path query path
* @return selected attribute value, or {@link Optional#empty()} when absent
*/
public static Optional<String> extractAttribute(final String xml, final String attributeName, final XmlPath path) {
if (xml == null || xml.isBlank()) return Optional.empty();
final var safePath = Objects.requireNonNull(path, "path nao pode ser nulo");
final var safeAttributeName = requireNonBlank(attributeName, "attributeName");
return withReader(xml, reader -> {
if (navigateToPath(reader, safePath)) {
return Optional.ofNullable(reader.getAttributeValue(null, safeAttributeName));
}
return Optional.empty();
});
}
/**
* Extracts attribute values from all matching elements using descendant path
* semantics.
*
* @param xml XML payload to query
* @param attributeName attribute local name
* @param path element local-name segments
* @return immutable list of present attribute values
*/
public static List<String> extractAttributes(final String xml, final String attributeName, final String... path) {
if (xml == null || xml.isBlank()) return List.of();
return extractAttributes(xml, attributeName, XmlPath.descendant(path));
}
/**
* Extracts attribute values from matching elements.
*
* @param xml XML payload to query
* @param attributeName attribute local name
* @param path query path
* @return immutable list of present attribute values
*/
public static List<String> extractAttributes(final String xml, final String attributeName, final XmlPath path) {
if (xml == null || xml.isBlank()) return List.of();
final var safePath = Objects.requireNonNull(path, "path nao pode ser nulo");
final var safeAttributeName = requireNonBlank(attributeName, "attributeName");
return withReader(xml, reader -> {
final var values = new ArrayList<String>();
collectPathMatches(reader, safePath, matchedReader -> {
final var value = matchedReader.getAttributeValue(null, safeAttributeName);
if (value != null) {
values.add(value);
}
skipCurrentElement(matchedReader);
});
return List.copyOf(values);
});
}
/**
* Extracts the first matching element as an XML fragment using descendant
* path semantics.
*
* @param xml XML payload to query
* @param path element local-name segments
* @return selected XML fragment, or {@link Optional#empty()} when absent
*/
public static Optional<String> extractElementXml(final String xml, final String... path) {
if (xml == null || xml.isBlank()) return Optional.empty();
return extractElementXml(xml, XmlPath.descendant(path));
}
/**
* Extracts the first matching element as an XML fragment.
* <p>
* The fragment writer preserves CDATA and declares inherited namespaces
* needed by the extracted element.
*
* @param xml XML payload to query
* @param path query path
* @return selected XML fragment, or {@link Optional#empty()} when absent
*/
public static Optional<String> extractElementXml(final String xml, final XmlPath path) {
if (xml == null || xml.isBlank()) return Optional.empty();
final var safePath = Objects.requireNonNull(path, "path nao pode ser nulo");
return withReader(xml, reader -> {
if (!navigateToPath(reader, safePath)) {
return Optional.empty();
}
final var sw = new StringWriter();
final var writer = OUTPUT_FACTORY.get().createXMLStreamWriter(sw);
try {
writeCurrentElement(reader, writer);
writer.flush();
return Optional.of(sw.toString());
} finally {
writer.close();
}
});
}
/**
* Extracts matching elements as XML fragments using descendant path
* semantics.
*
* @param xml XML payload to query
* @param path element local-name segments
* @return immutable list of selected XML fragments
*/
public static List<String> extractElementsXml(final String xml, final String... path) {
if (xml == null || xml.isBlank()) return List.of();
return extractElementsXml(xml, XmlPath.descendant(path));
}
/**
* Extracts matching elements as XML fragments.
*
* @param xml XML payload to query
* @param path query path
* @return immutable list of selected XML fragments
*/
public static List<String> extractElementsXml(final String xml, final XmlPath path) {
if (xml == null || xml.isBlank()) return List.of();
final var safePath = Objects.requireNonNull(path, "path nao pode ser nulo");
return withReader(xml, reader -> {
final var values = new ArrayList<String>();
collectPathMatches(reader, safePath, matchedReader -> {
final var sw = new StringWriter();
final var writer = OUTPUT_FACTORY.get().createXMLStreamWriter(sw);
try {
writeCurrentElement(matchedReader, writer);
writer.flush();
values.add(sw.toString());
} finally {
writer.close();
}
});
return List.copyOf(values);
});
}
/**
* Extracts matching element fragments and maps each fragment to a domain
* object or DTO.
*
* @param xml XML payload to query
* @param path query path for repeated elements
* @param mapper mapper that receives each matched element XML fragment
* @param <T> mapped result type
* @return immutable list of mapped values
*/
public static <T> List<T> extractEach(
final String xml,
final XmlPath path,
final Function<String, T> mapper
) {
if (xml == null || xml.isBlank()) return List.of();
final var safeMapper = Objects.requireNonNull(mapper, "mapper nao pode ser nulo");
final var elements = extractElementsXml(xml, path);
final var mapped = new ArrayList<T>(elements.size());
for (String element : elements) {
mapped.add(safeMapper.apply(element));
}
return List.copyOf(mapped);
}
private static void writeCurrentElement(XMLStreamReader reader, XMLStreamWriter writer) throws XMLStreamException {
final var namespaceScopes = new ArrayDeque<Map<String, String>>();
writeFragmentStartElement(reader, writer, namespaceScopes);
int depth = 1;
while (reader.hasNext() && depth > 0) {
int event = reader.next();
copyFragmentEvent(reader, writer, event, namespaceScopes);
if (event == XMLStreamConstants.START_ELEMENT) {
depth++;
} else if (event == XMLStreamConstants.END_ELEMENT) {
depth--;
}
}
}
private static void copyFragmentEvent(final XMLStreamReader reader, final XMLStreamWriter writer, final int event, final Deque<Map<String, String>> namespaceScopes) throws XMLStreamException {
switch (event) {
case XMLStreamConstants.START_ELEMENT -> writeFragmentStartElement(reader, writer, namespaceScopes);
case XMLStreamConstants.CHARACTERS -> {
if (!reader.isWhiteSpace()) writer.writeCharacters(reader.getText());
}
case XMLStreamConstants.CDATA -> writer.writeCData(reader.getText());
case XMLStreamConstants.END_ELEMENT -> {
writer.writeEndElement();
namespaceScopes.removeLast();
}
case XMLStreamConstants.COMMENT -> writer.writeComment(reader.getText());
case XMLStreamConstants.PROCESSING_INSTRUCTION -> writer.writeProcessingInstruction(
reader.getPITarget(),
reader.getPIData()
);
default -> {
}
}
}
private static void writeFragmentStartElement(
final XMLStreamReader reader,
final XMLStreamWriter writer,
final Deque<Map<String, String>> namespaceScopes
) throws XMLStreamException {
final var namespace = reader.getNamespaceURI();
final var prefix = safePrefix(reader.getPrefix());
final var declarations = new LinkedHashMap<String, String>();
if (namespace != null) {
writer.writeStartElement(prefix, reader.getLocalName(), namespace);
} else {
writer.writeStartElement(reader.getLocalName());
}
for (int i = 0; i < reader.getNamespaceCount(); i++) {
final var namespacePrefix = safePrefix(reader.getNamespacePrefix(i));
final var namespaceUri = reader.getNamespaceURI(i);
writer.writeNamespace(namespacePrefix, namespaceUri);
declarations.put(namespacePrefix, namespaceUri);
}
writeNamespaceIfMissing(writer, namespaceScopes, declarations, prefix, namespace);
for (int i = 0; i < reader.getAttributeCount(); i++) {
final var attributeNamespace = reader.getAttributeNamespace(i);
final var attributePrefix = safePrefix(reader.getAttributePrefix(i));
if (attributeNamespace != null && !attributePrefix.isBlank()) {
writeNamespaceIfMissing(writer, namespaceScopes, declarations, attributePrefix, attributeNamespace);
}
}
for (int i = 0; i < reader.getAttributeCount(); i++) {
final var attributeNamespace = reader.getAttributeNamespace(i);
final var attributePrefix = safePrefix(reader.getAttributePrefix(i));
if (attributeNamespace != null) {
writer.writeAttribute(attributePrefix, attributeNamespace, reader.getAttributeLocalName(i), reader.getAttributeValue(i));
} else {
writer.writeAttribute(reader.getAttributeLocalName(i), reader.getAttributeValue(i));
}
}
namespaceScopes.addLast(declarations);
}
private static void writeNamespaceIfMissing(
final XMLStreamWriter writer,
final Deque<Map<String, String>> namespaceScopes,
final Map<String, String> currentDeclarations,
final String prefix,
final String namespace
) throws XMLStreamException {
final var safePrefix = safePrefix(prefix);
if (namespace == null || namespace.isBlank() || isReservedNamespace(safePrefix)) {
return;
}
if (namespace.equals(currentDeclarations.get(safePrefix))
|| isNamespaceInScope(namespaceScopes, safePrefix, namespace)) {
return;
}
writer.writeNamespace(safePrefix, namespace);
currentDeclarations.put(safePrefix, namespace);
}
private static boolean isNamespaceInScope(
final Deque<Map<String, String>> namespaceScopes,
final String prefix,
final String namespace
) {
final var iterator = namespaceScopes.descendingIterator();
while (iterator.hasNext()) {
final var scope = iterator.next();
if (scope.containsKey(prefix)) {
return namespace.equals(scope.get(prefix));
}
}
return false;
}
private static boolean isReservedNamespace(final String prefix) {
return "xml".equals(prefix) || "xmlns".equals(prefix);
}
private static void skipCurrentElement(final XMLStreamReader reader) throws XMLStreamException {
int depth = 1;
while (reader.hasNext() && depth > 0) {
final var event = reader.next();
if (event == XMLStreamConstants.START_ELEMENT) {
depth++;
} else if (event == XMLStreamConstants.END_ELEMENT) {
depth--;
}
}
}
private static boolean navigateToPath(final XMLStreamReader reader, final XmlPath path) throws XMLStreamException {
return switch (path.mode()) {
case DIRECT -> navigateToDirectPath(reader, path);
case DESCENDANT -> navigateToDescendantPath(reader, path);
};
}
private static boolean navigateToDescendantPath(final XMLStreamReader reader, final XmlPath path) throws XMLStreamException {
final var segments = path.segmentsArray();
int currentDepth = 0;
int matches = 0;
while (reader.hasNext()) {
int event = reader.next();
if (event == XMLStreamConstants.START_ELEMENT) {
if (reader.getLocalName().equals(segments[currentDepth])) {
if (currentDepth == segments.length - 1) {
matches++;
if (matches == path.occurrence()) {
return true;
}
skipCurrentElement(reader);
} else {
currentDepth++;
}
}
} else if (event == XMLStreamConstants.END_ELEMENT) {
if (currentDepth > 0 && reader.getLocalName().equals(segments[currentDepth - 1])) {
currentDepth--;
}
}
}
return false;
}
private static boolean navigateToDirectPath(final XMLStreamReader reader, final XmlPath path) throws XMLStreamException {
final var currentPath = new ArrayDeque<String>();
int matches = 0;
while (reader.hasNext()) {
final var event = reader.next();
if (event == XMLStreamConstants.START_ELEMENT) {
currentPath.addLast(reader.getLocalName());
if (path.matches(currentPath)) {
matches++;
if (matches == path.occurrence()) {
return true;
}
}
} else if (event == XMLStreamConstants.END_ELEMENT) {
if (!currentPath.isEmpty()) {
currentPath.removeLast();
}
}
}
return false;
}
private static void collectPathMatches(
final XMLStreamReader reader,
final XmlPath path,
final MatchedElementAction action
) throws XMLStreamException {
switch (path.mode()) {
case DIRECT -> collectDirectPathMatches(reader, path, action);
case DESCENDANT -> collectDescendantPathMatches(reader, path, action);
}
}
private static void collectDescendantPathMatches(
final XMLStreamReader reader,
final XmlPath path,
final MatchedElementAction action
) throws XMLStreamException {
final var segments = path.segmentsArray();
int currentDepth = 0;
int matches = 0;
while (reader.hasNext()) {
final var event = reader.next();
if (event == XMLStreamConstants.START_ELEMENT) {
if (reader.getLocalName().equals(segments[currentDepth])) {
if (currentDepth == segments.length - 1) {
matches++;
if (path.shouldCollect(matches)) {
action.execute(reader);
if (path.occurrenceSpecified()) {
return;
}
} else {
skipCurrentElement(reader);
}
} else {
currentDepth++;
}
}
} else if (event == XMLStreamConstants.END_ELEMENT) {
if (currentDepth > 0 && reader.getLocalName().equals(segments[currentDepth - 1])) {
currentDepth--;
}
}
}
}
private static void collectDirectPathMatches(
final XMLStreamReader reader,
final XmlPath path,
final MatchedElementAction action
) throws XMLStreamException {
final var currentPath = new ArrayDeque<String>();
int matches = 0;
while (reader.hasNext()) {
final var event = reader.next();
if (event == XMLStreamConstants.START_ELEMENT) {
currentPath.addLast(reader.getLocalName());
if (path.matches(currentPath)) {
matches++;
if (path.shouldCollect(matches)) {
action.execute(reader);
currentPath.removeLast();
if (path.occurrenceSpecified()) {
return;
}
} else {
skipCurrentElement(reader);
currentPath.removeLast();
}
}
} else if (event == XMLStreamConstants.END_ELEMENT) {
if (!currentPath.isEmpty()) {
currentPath.removeLast();
}
}
}
}
private static String extractText(XMLStreamReader reader) throws XMLStreamException {
final var text = new StringBuilder();
int depth = 1;
while (reader.hasNext() && depth > 0) {
int event = reader.next();
if (event == XMLStreamConstants.CHARACTERS || event == XMLStreamConstants.CDATA) {
text.append(reader.getText());
} else if (event == XMLStreamConstants.START_ELEMENT) {
depth++;
} else if (event == XMLStreamConstants.END_ELEMENT) {
depth--;
}
}
return text.toString().trim();
}
private static String applyInsertions(String xml, List<InsertOperation> operations) {
if (xml == null || xml.isBlank()) return xml;
if (operations == null || operations.isEmpty()) return xml;
return withReader(xml, reader -> {
List<InsertState> states = new ArrayList<>(operations.size());
for (InsertOperation operation : operations) {
states.add(new InsertState(operation));
}
Deque<String> currentPath = new ArrayDeque<>();
int depth = 0;
StringWriter sw = new StringWriter(xml.length() + (operations.size() * 96));
XMLStreamWriter writer = OUTPUT_FACTORY.get().createXMLStreamWriter(sw);
try {
while (reader.hasNext()) {
int event = reader.next();
switch (event) {
case XMLStreamConstants.START_ELEMENT -> {
depth++;
final var localName = reader.getLocalName();
currentPath.addLast(localName);
for (InsertState state : states) {
state.onStartElement(currentPath, depth, localName, writer);
}
if (!isInsideSkippedSubtree(states, depth)) {
writeStartElement(reader, writer);
}
}
case XMLStreamConstants.END_ELEMENT -> {
boolean skipCurrentEnd = isInsideSkippedSubtree(states, depth);
if (!skipCurrentEnd) {
for (InsertState state : states) {
state.beforeEndElement(depth, writer);
}
writer.writeEndElement();
for (InsertState state : states) {
state.afterEndElement(depth, writer);
}
}
for (InsertState state : states) {
state.onEndElement(depth);
}
currentPath.removeLast();
depth--;
}
case XMLStreamConstants.CHARACTERS, XMLStreamConstants.SPACE -> {
if (!isInsideSkippedSubtree(states, depth)) {
writer.writeCharacters(reader.getText());
}
}
case XMLStreamConstants.CDATA -> {
if (!isInsideSkippedSubtree(states, depth)) {
writer.writeCData(reader.getText());
}
}
case XMLStreamConstants.COMMENT -> {
if (!isInsideSkippedSubtree(states, depth)) {
writer.writeComment(reader.getText());
}
}
case XMLStreamConstants.PROCESSING_INSTRUCTION -> {
if (!isInsideSkippedSubtree(states, depth)) {
writer.writeProcessingInstruction(reader.getPITarget(), reader.getPIData());
}
}
default -> {
// Ignora eventos que não mudam estrutura de tags (DTD, START_DOCUMENT, END_DOCUMENT, etc.)
}
}
}
writer.flush();
validateAppliedOperations(states);
return sw.toString();
} finally {
writer.close();
}
});
}
private static boolean isInsideSkippedSubtree(List<InsertState> states, int depth) {
for (InsertState state : states) {
if (state.isSkippingDepth(depth)) {
return true;
}
}
return false;
}
private static void validateAppliedOperations(List<InsertState> states) {
for (InsertState state : states) {
if (!state.applied()) {
throw new XmlException("Nao foi possível inserir tag em " + state.operation().path().description() + " (posicao=" + state.operation().position() + ")", null);
}
}
}
private static void writeStartElement(XMLStreamReader reader, XMLStreamWriter writer) throws XMLStreamException {
final var namespace = reader.getNamespaceURI();
final var prefix = safePrefix(reader.getPrefix());
if (namespace != null) {
writer.writeStartElement(prefix, reader.getLocalName(), namespace);
} else {
writer.writeStartElement(reader.getLocalName());
}
for (int i = 0; i < reader.getNamespaceCount(); i++) {
writer.writeNamespace(safePrefix(reader.getNamespacePrefix(i)), reader.getNamespaceURI(i));
}
for (int i = 0; i < reader.getAttributeCount(); i++) {
final var attributeNamespace = reader.getAttributeNamespace(i);
final var attributePrefix = safePrefix(reader.getAttributePrefix(i));
if (attributeNamespace != null) {
writer.writeAttribute(attributePrefix, attributeNamespace, reader.getAttributeLocalName(i), reader.getAttributeValue(i));
} else {
writer.writeAttribute(reader.getAttributeLocalName(i), reader.getAttributeValue(i));
}
}
}
private static String safePrefix(String prefix) {
return prefix == null ? "" : prefix;
}
private static void writeNode(XMLStreamWriter writer, XmlNode node) throws XMLStreamException {
writer.writeStartElement(node.name());
for (Map.Entry<String, String> attribute : node.attributes().entrySet()) {
writer.writeAttribute(attribute.getKey(), attribute.getValue());
}
if (node.value() != null) {
writer.writeCharacters(node.value());
}
for (XmlNode child : node.children()) {
writeNode(writer, child);
}
writer.writeEndElement();
}
private static void copyEvent(XMLStreamReader reader, XMLStreamWriter writer, int event) throws XMLStreamException {
switch (event) {
case XMLStreamConstants.START_ELEMENT -> writeStartElement(reader, writer);
case XMLStreamConstants.CHARACTERS -> {
if (!reader.isWhiteSpace()) writer.writeCharacters(reader.getText());
}
case XMLStreamConstants.CDATA -> writer.writeCData(reader.getText());
case XMLStreamConstants.END_ELEMENT -> writer.writeEndElement();
case XMLStreamConstants.COMMENT -> writer.writeComment(reader.getText());
case XMLStreamConstants.PROCESSING_INSTRUCTION -> writer.writeProcessingInstruction(reader.getPITarget(), reader.getPIData());
default -> {
}
}
}
private static <T> T withReader(String xml, ReaderAction<T> action) {
if (xml == null || xml.isBlank()) return null;
try (StringReader sr = new StringReader(xml)) {
final var reader = INPUT_FACTORY.get().createXMLStreamReader(sr);
try {
return action.execute(reader);
} finally {
reader.close();
}
} catch (XMLStreamException e) {
throw new XmlException("Erro Woodstox no processamento", e);
}
}
private static String requireNonBlank(String value, String fieldName) {
if (value == null || value.isBlank()) {
throw new XmlException(fieldName + " nao pode ser nulo ou vazio");
}
return value;
}
private static String[] requirePath(String[] path) {
if (path == null || path.length == 0) {
throw new XmlException("path precisa ter ao menos um elemento");
}
String[] safePath = new String[path.length];
for (int i = 0; i < path.length; i++) {
safePath[i] = requireNonBlank(path[i], "path[" + i + "]");
}
return safePath;
}
private enum InsertPosition {
CHILD,
BEFORE,
AFTER
}
private enum XmlPathMode {
DIRECT,
DESCENDANT
}
@FunctionalInterface
private interface ReaderAction<T> {
T execute(XMLStreamReader reader) throws XMLStreamException;
}
@FunctionalInterface
private interface MatchedElementAction {
void execute(XMLStreamReader reader) throws XMLStreamException;
}
/**
* Lazy query result produced by {@link XmlUtil#select(String, XmlPath)} or
* {@link XmlDocument#select(XmlPath)}.
* <p>
* A selection can be materialized as text, XML fragments, typed values, or
* attributes. Materialization methods parse the source XML and return
* immutable results.
*/
public static final class XmlSelection {
private final String xml;
private final XmlPath path;
private final String attributeName;
private XmlSelection(final String xml, final XmlPath path, final String attributeName) {
this.xml = xml;
this.path = Objects.requireNonNull(path, "path nao pode ser nulo");
this.attributeName = attributeName;
}
/**
* Narrows this selection to an attribute on each matched element.
*
* @param attributeName attribute local name
* @return attribute selection
*/
public XmlSelection attribute(final String attributeName) {
return new XmlSelection(xml, path, requireNonBlank(attributeName, "attributeName"));
}
/**
* Checks whether this selection resolves to at least one element or
* attribute.
*
* @return {@code true} when a selected element or attribute exists
*/
public boolean exists() {
if (attributeName != null) {
return string().isPresent();
}
return elementXml().isPresent();
}
/**
* Counts selected elements, or present selected attributes when the
* selection was narrowed with {@link #attribute(String)}.
*
* @return number of selected elements or present attributes
*/
public int count() {
if (attributeName != null) {
return strings().size();
}
return elementsXml().size();
}
/**
* Reads the first selected text or attribute value.
*
* @return selected value, or {@link Optional#empty()} when absent
*/
public Optional<String> string() {
if (attributeName != null) {
return XmlUtil.extractAttribute(xml, attributeName, path);
}
return XmlUtil.extractValue(xml, path);
}
/**
* Alias for {@link #string()} using XML terminology.
*
* @return selected text, or {@link Optional#empty()} when absent
*/
public Optional<String> text() {
return string();
}
/**
* Alias for {@link #string()} using conversion-style naming.
*
* @return selected value, or {@link Optional#empty()} when absent
*/
public Optional<String> asString() {
return string();
}
/**
* Reads all selected text or attribute values.
*
* @return immutable list of selected values
*/
public List<String> strings() {
if (attributeName != null) {
return XmlUtil.extractAttributes(xml, attributeName, path);
}
return XmlUtil.extractValues(xml, path);
}
/**
* Alias for {@link #strings()} using XML terminology.
*
* @return immutable list of selected text values
*/
public List<String> texts() {
return strings();
}
/**
* Alias for {@link #strings()} using conversion-style naming.
*
* @return immutable list of selected values
*/
public List<String> asStrings() {
return strings();
}
/**
* Converts the first selected value to {@link Integer}.
*
* @return converted value, or {@link Optional#empty()} when absent
*/
public Optional<Integer> integer() {
return as(Integer::valueOf, "Integer");
}
/**
* Alias for {@link #integer()}.
*
* @return converted value, or {@link Optional#empty()} when absent
*/
public Optional<Integer> asInteger() {
return integer();
}
/**
* Converts all selected values to {@link Integer}.
*
* @return immutable list of converted values
*/
public List<Integer> integers() {
return listAs(Integer::valueOf, "Integer");
}
/**
* Alias for {@link #integers()}.
*
* @return immutable list of converted values
*/
public List<Integer> asIntegers() {
return integers();
}
/**
* Converts the first selected value to {@link Long}.
*
* @return converted value, or {@link Optional#empty()} when absent
*/
public Optional<Long> longValue() {
return as(Long::valueOf, "Long");
}
/**
* Alias for {@link #longValue()}.
*
* @return converted value, or {@link Optional#empty()} when absent
*/
public Optional<Long> asLong() {
return longValue();
}
/**
* Converts all selected values to {@link Long}.
*
* @return immutable list of converted values
*/
public List<Long> longValues() {
return listAs(Long::valueOf, "Long");
}
/**
* Alias for {@link #longValues()}.
*
* @return immutable list of converted values
*/
public List<Long> asLongs() {
return longValues();
}
/**
* Converts the first selected value to {@link BigDecimal}.
*
* @return converted value, or {@link Optional#empty()} when absent
*/
public Optional<BigDecimal> decimal() {
return as(BigDecimal::new, "BigDecimal");
}
/**
* Alias for {@link #decimal()}.
*
* @return converted value, or {@link Optional#empty()} when absent
*/
public Optional<BigDecimal> asDecimal() {
return decimal();
}
/**
* Converts all selected values to {@link BigDecimal}.
*
* @return immutable list of converted values
*/
public List<BigDecimal> decimals() {
return listAs(BigDecimal::new, "BigDecimal");
}
/**
* Alias for {@link #decimals()}.
*
* @return immutable list of converted values
*/
public List<BigDecimal> asDecimals() {
return decimals();
}
/**
* Converts the first selected value to {@link Boolean}.
* <p>
* Accepted values are {@code true}, {@code false}, {@code 1}, and
* {@code 0}, ignoring surrounding whitespace and case.
*
* @return converted value, or {@link Optional#empty()} when absent
*/
public Optional<Boolean> bool() {
return as(XmlSelection::parseBoolean, "Boolean");
}
/**
* Alias for {@link #bool()}.
*
* @return converted value, or {@link Optional#empty()} when absent
*/
public Optional<Boolean> asBoolean() {
return bool();
}
/**
* Converts all selected values to {@link Boolean}.
*
* @return immutable list of converted values
*/
public List<Boolean> booleans() {
return listAs(XmlSelection::parseBoolean, "Boolean");
}
/**
* Alias for {@link #booleans()}.
*
* @return immutable list of converted values
*/
public List<Boolean> asBooleans() {
return booleans();
}
/**
* Converts the first selected value to {@link Instant} using
* {@link Instant#parse(CharSequence)}.
*
* @return converted value, or {@link Optional#empty()} when absent
*/
public Optional<Instant> instant() {
return as(Instant::parse, "Instant");
}
/**
* Alias for {@link #instant()}.
*
* @return converted value, or {@link Optional#empty()} when absent
*/
public Optional<Instant> asInstant() {
return instant();
}
/**
* Converts all selected values to {@link Instant}.
*
* @return immutable list of converted values
*/
public List<Instant> instants() {
return listAs(Instant::parse, "Instant");
}
/**
* Alias for {@link #instants()}.
*
* @return immutable list of converted values
*/
public List<Instant> asInstants() {
return instants();
}
/**
* Reads the first selected element as an XML fragment.
*
* @return selected XML fragment, or {@link Optional#empty()} when absent
* @throws XmlException when this selection was narrowed to an attribute
*/
public Optional<String> elementXml() {
requireElementSelection();
return XmlUtil.extractElementXml(xml, path);
}
/**
* Alias for {@link #elementXml()}.
*
* @return selected XML fragment, or {@link Optional#empty()} when absent
*/
public Optional<String> xml() {
return elementXml();
}
/**
* Reads the first selected element as an {@link XmlElement} wrapper.
*
* @return selected XML element, or {@link Optional#empty()} when absent
*/
public Optional<XmlElement> element() {
return elementXml().map(XmlElement::of);
}
/**
* Reads all selected elements as XML fragments.
*
* @return immutable list of selected XML fragments
* @throws XmlException when this selection was narrowed to an attribute
*/
public List<String> elementsXml() {
requireElementSelection();
return XmlUtil.extractElementsXml(xml, path);
}
/**
* Alias for {@link #elementsXml()}.
*
* @return immutable list of selected XML fragments
*/
public List<String> xmls() {
return elementsXml();
}
/**
* Reads all selected elements as {@link XmlElement} wrappers.
*
* @return immutable list of selected XML elements
*/
public List<XmlElement> elements() {
final var elementXmls = elementsXml();
final var elements = new ArrayList<XmlElement>(elementXmls.size());
for (String elementXml : elementXmls) {
elements.add(XmlElement.of(elementXml));
}
return List.copyOf(elements);
}
/**
* Converts the first selected value using a custom mapper.
*
* @param mapper value mapper
* @param <T> mapped type
* @return mapped value, or {@link Optional#empty()} when absent
*/
public <T> Optional<T> as(final Function<String, T> mapper) {
return as(mapper, "custom");
}
/**
* Converts all selected values using a custom mapper.
*
* @param mapper value mapper
* @param <T> mapped type
* @return immutable list of mapped values
*/
public <T> List<T> listAs(final Function<String, T> mapper) {
return listAs(mapper, "custom");
}
private <T> Optional<T> as(final Function<String, T> mapper, final String targetType) {
final var safeMapper = Objects.requireNonNull(mapper, "mapper nao pode ser nulo");
return string().map(value -> convertValue(value, safeMapper, targetType));
}
private <T> List<T> listAs(final Function<String, T> mapper, final String targetType) {
final var safeMapper = Objects.requireNonNull(mapper, "mapper nao pode ser nulo");
final var values = strings();
final var converted = new ArrayList<T>(values.size());
for (String value : values) {
converted.add(convertValue(value, safeMapper, targetType));
}
return List.copyOf(converted);
}
private void requireElementSelection() {
if (attributeName != null) {
throw new XmlException("elementXml nao pode ser usado depois de attribute");
}
}
private static <T> T convertValue(
final String value,
final Function<String, T> mapper,
final String targetType
) {
try {
return mapper.apply(value);
} catch (RuntimeException e) {
throw new XmlException("Nao foi possivel converter valor XML para " + targetType, e);
}
}
private static Boolean parseBoolean(final String value) {
final var normalized = value.trim().toLowerCase();
return switch (normalized) {
case "true", "1" -> true;
case "false", "0" -> false;
default -> throw new XmlException("Valor booleano invalido: " + value);
};
}
}
/**
* Wrapper around a selected XML fragment.
* <p>
* This type is useful when processing repeated blocks because it allows
* querying the fragment again without exposing callers to the lower-level
* extract* shortcuts.
*/
public static final class XmlElement {
private final String xml;
private XmlElement(final String xml) {
this.xml = requireNonBlank(xml, "xml");
}
/**
* Wraps an XML fragment as an {@link XmlElement}.
*
* @param xml XML fragment
* @return element wrapper
*/
public static XmlElement of(final String xml) {
return new XmlElement(xml);
}
/**
* Returns the wrapped XML fragment.
*
* @return XML fragment
*/
public String xml() {
return xml;
}
/**
* Reads the text content of the wrapped element itself.
*
* @return element text, or {@link Optional#empty()} when the fragment has
* no start element
*/
public Optional<String> text() {
return withReader(xml, reader -> {
while (reader.hasNext()) {
final var event = reader.next();
if (event == XMLStreamConstants.START_ELEMENT) {
return Optional.of(extractText(reader));
}
}
return Optional.empty();
});
}
/**
* Selects nodes inside this XML fragment using descendant path
* semantics.
*
* @param path element local-name segments
* @return lazy materialization facade for the selected nodes
*/
public XmlSelection select(final String... path) {
return XmlUtil.select(xml, path);
}
/**
* Selects nodes inside this XML fragment using an explicit path.
*
* @param path query path
* @return lazy materialization facade for the selected nodes
*/
public XmlSelection select(final XmlPath path) {
return XmlUtil.select(xml, path);
}
/**
* Reads the first text value selected inside this fragment.
*
* @param path element local-name segments
* @return selected text, or {@link Optional#empty()} when absent
*/
public Optional<String> text(final String... path) {
return select(path).text();
}
/**
* Reads an attribute from the wrapped element itself.
*
* @param attributeName attribute local name
* @return attribute value, or {@link Optional#empty()} when absent
*/
public Optional<String> attribute(final String attributeName) {
final var safeAttributeName = requireNonBlank(attributeName, "attributeName");
return withReader(xml, reader -> {
while (reader.hasNext()) {
final var event = reader.next();
if (event == XMLStreamConstants.START_ELEMENT) {
return Optional.ofNullable(reader.getAttributeValue(null, safeAttributeName));
}
}
return Optional.empty();
});
}
/**
* Reads an attribute from the first element selected inside this
* fragment.
*
* @param attributeName attribute local name
* @param path element local-name segments
* @return attribute value, or {@link Optional#empty()} when absent
*/
public Optional<String> attribute(final String attributeName, final String... path) {
return select(path).attribute(attributeName).text();
}
/**
* Creates an editor for this XML fragment.
*
* @return immutable editor facade
*/
public XmlEditor edit() {
return XmlUtil.edit(xml);
}
@Override
public String toString() {
return xml;
}
}
/**
* Immutable fluent API for in-memory structural XML insertions.
* <p>
* Example:
* <pre>
* String out = XmlUtil.edit(xml)
* .addChild(XmlUtil.XmlNode.simple("item", "A"), "list")
* .addBefore(XmlUtil.XmlNode.simple("meta", "v1"), "header")
* .addAfter(XmlUtil.XmlNode.simple("checksum", "ok"), "footer")
* .build();
* </pre>
*/
public static final class XmlEditor {
private final String sourceXml;
private final List<InsertOperation> operations;
private XmlEditor(String sourceXml, List<InsertOperation> operations) {
this.sourceXml = sourceXml;
this.operations = operations;
}
/**
* Creates an editor for a source XML payload.
*
* @param sourceXml source XML payload
* @return immutable editor
*/
public static XmlEditor from(final String sourceXml) {
return new XmlEditor(sourceXml, List.of());
}
/**
* Adds or replaces a direct child under the first matching target path.
*
* @param node node to insert
* @param targetPath direct target path segments
* @return new editor instance with the queued operation
*/
public XmlEditor addChild(final XmlNode node, final String... targetPath) {
return addChild(node, XmlPath.of(targetPath));
}
/**
* Inserts a node before the first matching target path.
*
* @param node node to insert
* @param targetPath direct target path segments
* @return new editor instance with the queued operation
*/
public XmlEditor addBefore(final XmlNode node, final String... targetPath) {
return addBefore(node, XmlPath.of(targetPath));
}
/**
* Inserts a node after the first matching target path.
*
* @param node node to insert
* @param targetPath direct target path segments
* @return new editor instance with the queued operation
*/
public XmlEditor addAfter(final XmlNode node, final String... targetPath) {
return addAfter(node, XmlPath.of(targetPath));
}
/**
* Adds or replaces a direct child under the matching target path.
*
* @param node node to insert
* @param targetPath target path
* @return new editor instance with the queued operation
*/
public XmlEditor addChild(final XmlNode node, final XmlPath targetPath) {
return appendOperation(new InsertOperation(InsertPosition.CHILD, targetPath, node));
}
/**
* Inserts a node before the matching target path.
*
* @param node node to insert
* @param targetPath target path
* @return new editor instance with the queued operation
*/
public XmlEditor addBefore(final XmlNode node, final XmlPath targetPath) {
return appendOperation(new InsertOperation(InsertPosition.BEFORE, targetPath, node));
}
/**
* Inserts a node after the matching target path.
*
* @param node node to insert
* @param targetPath target path
* @return new editor instance with the queued operation
*/
public XmlEditor addAfter(final XmlNode node, final XmlPath targetPath) {
return appendOperation(new InsertOperation(InsertPosition.AFTER, targetPath, node));
}
/**
* Applies all queued insertions and returns the resulting XML.
*
* @return edited XML
*/
public String build() {
return applyInsertions(sourceXml, operations);
}
/**
* Applies all queued insertions asynchronously using the common pool.
*
* @return future containing the edited XML
*/
public CompletableFuture<String> buildAsync() {
return CompletableFuture.supplyAsync(this::build);
}
/**
* Applies all queued insertions asynchronously using the provided
* executor.
*
* @param executor executor used to run the edit operation
* @return future containing the edited XML
*/
public CompletableFuture<String> buildAsync(final Executor executor) {
Objects.requireNonNull(executor, "executor nao pode ser nulo");
return CompletableFuture.supplyAsync(this::build, executor);
}
private XmlEditor appendOperation(InsertOperation operation) {
Objects.requireNonNull(operation, "operation nao pode ser nulo");
List<InsertOperation> newOperations = new ArrayList<>(operations.size() + 1);
newOperations.addAll(operations);
newOperations.add(operation);
return new XmlEditor(sourceXml, List.copyOf(newOperations));
}
}
/**
* Immutable XML path descriptor used by query and edit operations.
* <p>
* Paths match element local names. Namespace URI matching is not part of
* this path type; namespaced XML is queried by local name.
*/
public static final class XmlPath {
private final List<String> segments;
private final int occurrence;
private final XmlPathMode mode;
private final boolean occurrenceSpecified;
private XmlPath(List<String> segments, int occurrence, XmlPathMode mode, boolean occurrenceSpecified) {
this.segments = segments;
this.occurrence = occurrence;
this.mode = mode;
this.occurrenceSpecified = occurrenceSpecified;
}
/**
* Creates a direct-child path.
* <p>
* This method is kept as the default for editor compatibility. Query
* shortcuts that receive {@code String...} use {@link #descendant(String...)}
* instead to preserve legacy extraction behavior.
*
* @param segments element local-name segments
* @return direct-child path
*/
public static XmlPath of(String... segments) {
return direct(segments);
}
/**
* Creates a path that requires each segment to be a direct child of the
* previous segment.
*
* @param segments element local-name segments
* @return direct-child path
*/
public static XmlPath direct(String... segments) {
return from(XmlPathMode.DIRECT, segments);
}
/**
* Creates a path that allows unrelated intermediate elements between
* provided segments.
*
* @param segments element local-name segments
* @return descendant path
*/
public static XmlPath descendant(String... segments) {
return from(XmlPathMode.DESCENDANT, segments);
}
private static XmlPath from(final XmlPathMode mode, final String... segments) {
String[] safeSegments = requirePath(segments);
List<String> normalized = new ArrayList<>(safeSegments.length);
Collections.addAll(normalized, safeSegments);
return new XmlPath(List.copyOf(normalized), 1, mode, false);
}
/**
* Restricts the path to the nth match, using a one-based index.
* <p>
* When no occurrence is specified, list materialization methods collect
* all matches.
*
* @param occurrence one-based occurrence index
* @return path restricted to the requested occurrence
*/
public XmlPath occurrence(int occurrence) {
if (occurrence < 1) {
throw new XmlException("occurrence precisa ser >= 1");
}
return new XmlPath(segments, occurrence, mode, true);
}
private int occurrence() {
return occurrence;
}
private boolean occurrenceSpecified() {
return occurrenceSpecified;
}
private boolean shouldCollect(final int match) {
return !occurrenceSpecified || match == occurrence;
}
private XmlPathMode mode() {
return mode;
}
private String[] segmentsArray() {
return segments.toArray(String[]::new);
}
private boolean matches(Deque<String> currentPath) {
if (currentPath.size() < segments.size()) {
return false;
}
int offset = currentPath.size() - segments.size();
int index = 0;
for (String item : currentPath) {
if (index >= offset) {
if (!segments.get(index - offset).equals(item)) {
return false;
}
}
index++;
}
return true;
}
private String description() {
return String.join("/", segments) + "[" + occurrence + "]";
}
}
/**
* Immutable XML node used by {@link XmlEditor} insertion operations.
*/
public static final class XmlNode {
private final String name;
private final Map<String, String> attributes;
private final String value;
private final List<XmlNode> children;
private XmlNode(String name, Map<String, String> attributes, String value, List<XmlNode> children) {
this.name = name;
this.attributes = attributes;
this.value = value;
this.children = children;
}
/**
* Starts building an element node.
*
* @param name element local name
* @return node builder
*/
public static Builder element(String name) {
return new Builder(name);
}
/**
* Creates an element with text content and no children.
*
* @param name element local name
* @param value text value
* @return immutable XML node
*/
public static XmlNode simple(String name, String value) {
return element(name).value(value).build();
}
private String name() {
return name;
}
private Map<String, String> attributes() {
return attributes;
}
private String value() {
return value;
}
private List<XmlNode> children() {
return children;
}
/**
* Builder for immutable {@link XmlNode} instances.
*/
public static final class Builder {
private final String name;
private final Map<String, String> attributes = new LinkedHashMap<>();
private final List<XmlNode> children = new ArrayList<>();
private String value;
private Builder(String name) {
this.name = requireNonBlank(name, "name");
}
/**
* Adds an attribute to the node.
*
* @param name attribute local name
* @param value attribute value
* @return this builder
*/
public Builder attribute(String name, String value) {
attributes.put(requireNonBlank(name, "attributeName"), Objects.requireNonNull(value, "attributeValue"));
return this;
}
/**
* Sets the node text value.
*
* @param value text value
* @return this builder
*/
public Builder value(String value) {
this.value = Objects.requireNonNull(value, "value nao pode ser nulo");
return this;
}
/**
* Adds a child node.
*
* @param node child node
* @return this builder
*/
public Builder child(XmlNode node) {
children.add(Objects.requireNonNull(node, "child nao pode ser nulo"));
return this;
}
/**
* Builds an immutable node snapshot.
*
* @return immutable XML node
*/
public XmlNode build() {
return new XmlNode(
name,
Collections.unmodifiableMap(new LinkedHashMap<>(attributes)),
value,
List.copyOf(children)
);
}
}
}
/**
* Immutable facade over an XML payload.
* <p>
* This is the recommended entrypoint for new code:
* <pre>
* var document = XmlUtil.document(xml);
* var number = document.select("NFe", "det", "prod", "cProd").text();
* </pre>
*/
public static class XmlDocument {
private final String xml;
private XmlDocument(final String xml) {
this.xml = xml;
}
/**
* Selects nodes using descendant path semantics.
*
* @param path element local-name segments
* @return lazy materialization facade for the selected nodes
*/
public XmlSelection select(final String... path) {
return XmlUtil.select(xml, path);
}
/**
* Selects nodes using an explicit path.
*
* @param path query path
* @return lazy materialization facade for the selected nodes
*/
public XmlSelection select(final XmlPath path) {
return XmlUtil.select(xml, path);
}
/**
* Compatibility shortcut for extracting a single attribute value.
*
* @param attributeName attribute local name
* @param path element local-name segments
* @return attribute value, or {@link Optional#empty()} when absent
*/
public Optional<String> extractAttribute(String attributeName, String... path) {
return XmlUtil.extractAttribute(xml, attributeName, path);
}
/**
* Compatibility shortcut for extracting a single attribute value.
*
* @param attributeName attribute local name
* @param path query path
* @return attribute value, or {@link Optional#empty()} when absent
*/
public Optional<String> extractAttribute(final String attributeName, final XmlPath path) {
return XmlUtil.extractAttribute(xml, attributeName, path);
}
/**
* Compatibility shortcut for extracting multiple attribute values.
*
* @param attributeName attribute local name
* @param path element local-name segments
* @return immutable list of present attribute values
*/
public List<String> extractAttributes(final String attributeName, final String... path) {
return XmlUtil.extractAttributes(xml, attributeName, path);
}
/**
* Compatibility shortcut for extracting multiple attribute values.
*
* @param attributeName attribute local name
* @param path query path
* @return immutable list of present attribute values
*/
public List<String> extractAttributes(final String attributeName, final XmlPath path) {
return XmlUtil.extractAttributes(xml, attributeName, path);
}
/**
* Compatibility shortcut for extracting a single text value.
*
* @param path element local-name segments
* @return text value, or {@link Optional#empty()} when absent
*/
public Optional<String> extractValue(String... path) {
return XmlUtil.extractValue(xml, path);
}
/**
* Compatibility shortcut for extracting a single text value.
*
* @param path query path
* @return text value, or {@link Optional#empty()} when absent
*/
public Optional<String> extractValue(final XmlPath path) {
return XmlUtil.extractValue(xml, path);
}
/**
* Compatibility shortcut for extracting multiple text values.
*
* @param path element local-name segments
* @return immutable list of text values
*/
public List<String> extractValues(final String... path) {
return XmlUtil.extractValues(xml, path);
}
/**
* Compatibility shortcut for extracting multiple text values.
*
* @param path query path
* @return immutable list of text values
*/
public List<String> extractValues(final XmlPath path) {
return XmlUtil.extractValues(xml, path);
}
/**
* Compatibility shortcut for extracting a single XML fragment.
*
* @param path element local-name segments
* @return XML fragment, or {@link Optional#empty()} when absent
*/
public Optional<String> extractElementXml(String... path) {
return XmlUtil.extractElementXml(xml, path);
}
/**
* Compatibility shortcut for extracting a single XML fragment.
*
* @param path query path
* @return XML fragment, or {@link Optional#empty()} when absent
*/
public Optional<String> extractElementXml(final XmlPath path) {
return XmlUtil.extractElementXml(xml, path);
}
/**
* Compatibility shortcut for extracting multiple XML fragments.
*
* @param path element local-name segments
* @return immutable list of XML fragments
*/
public List<String> extractElementsXml(final String... path) {
return XmlUtil.extractElementsXml(xml, path);
}
/**
* Compatibility shortcut for extracting multiple XML fragments.
*
* @param path query path
* @return immutable list of XML fragments
*/
public List<String> extractElementsXml(final XmlPath path) {
return XmlUtil.extractElementsXml(xml, path);
}
/**
* Compatibility shortcut for mapping repeated XML fragments.
*
* @param path query path for repeated elements
* @param mapper mapper that receives each matched element XML fragment
* @param <T> mapped result type
* @return immutable list of mapped values
*/
public <T> List<T> extractEach(final XmlPath path, final Function<String, T> mapper) {
return XmlUtil.extractEach(xml, path, mapper);
}
/**
* Creates an editor for this XML payload.
*
* @return immutable editor facade
*/
public XmlEditor edit() {
return XmlUtil.edit(xml);
}
}
/**
* Backward-compatible name for the original fluent facade.
* <p>
* New code should use {@link XmlDocument}, created through
* {@link XmlUtil#document(String)}.
*/
public static final class FluentXml extends XmlDocument {
private FluentXml(final String xml) {
super(xml);
}
}
private record InsertOperation(InsertPosition position, XmlPath path, XmlNode node) {
private InsertOperation {
Objects.requireNonNull(position, "position nao pode ser nulo");
Objects.requireNonNull(path, "path nao pode ser nulo");
Objects.requireNonNull(node, "node nao pode ser nulo");
}
}
private static final class InsertState {
private final InsertOperation operation;
private int matches;
private int armedDepth = -1;
private int skipDepth = -1;
private boolean applied;
private InsertState(InsertOperation operation) {
this.operation = operation;
}
private void onStartElement(Deque<String> currentPath, int depth, String localName, XMLStreamWriter writer) throws XMLStreamException {
if (applied || armedDepth != -1) {
if (shouldSkipExistingChild(depth, localName)) {
skipDepth = depth;
}
return;
}
if (!operation.path().matches(currentPath)) {
return;
}
matches++;
if (matches != operation.path().occurrence()) {
return;
}
if (operation.position() == InsertPosition.BEFORE) {
writeNode(writer, operation.node());
applied = true;
return;
}
armedDepth = depth;
}
private boolean shouldSkipExistingChild(int depth, String localName) {
return operation.position() == InsertPosition.CHILD
&& armedDepth > 0
&& skipDepth == -1
&& depth == armedDepth + 1
&& operation.node().name().equals(localName);
}
private void beforeEndElement(int depth, XMLStreamWriter writer) throws XMLStreamException {
if (applied || armedDepth != depth || operation.position() != InsertPosition.CHILD) {
return;
}
writeNode(writer, operation.node());
applied = true;
armedDepth = -1;
}
private void afterEndElement(int depth, XMLStreamWriter writer) throws XMLStreamException {
if (applied || armedDepth != depth || operation.position() != InsertPosition.AFTER) {
return;
}
writeNode(writer, operation.node());
applied = true;
armedDepth = -1;
}
private void onEndElement(int depth) {
if (skipDepth == depth) {
skipDepth = -1;
}
}
private boolean isSkippingDepth(int depth) {
return skipDepth != -1 && depth >= skipDepth;
}
private boolean applied() {
return applied;
}
private InsertOperation operation() {
return operation;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment