001package com.streamconverter.command.impl.xml; 002 003import com.streamconverter.command.AbstractStreamCommand; 004import com.streamconverter.command.rule.IRule; 005import com.streamconverter.path.TreePath; 006import com.streamconverter.security.SecureXmlConfiguration; 007import java.io.IOException; 008import java.io.InputStream; 009import java.io.OutputStream; 010import java.util.ArrayList; 011import java.util.List; 012import javax.xml.stream.XMLEventFactory; 013import javax.xml.stream.XMLEventReader; 014import javax.xml.stream.XMLEventWriter; 015import javax.xml.stream.XMLInputFactory; 016import javax.xml.stream.XMLOutputFactory; 017import javax.xml.stream.XMLStreamException; 018import javax.xml.stream.events.XMLEvent; 019import org.slf4j.Logger; 020import org.slf4j.LoggerFactory; 021 022/** 023 * XML Navigate Command for applying transformations to XML data. 024 * 025 * <p>This command navigates through XML structures and applies transformations using rules while 026 * preserving the overall XML structure. It identifies specific elements using TreePath 027 * slash-delimited path expressions (e.g., {@code product/name}) and applies {@link IRule} 028 * transformations to the text content of matching nodes. 029 * 030 * <p>The full XML event stream — including the XML declaration, all elements, attributes, and text 031 * nodes — is written to the output. Only character data at nodes whose path exactly matches the 032 * configured {@link TreePath} is transformed by the rule; all other events are passed through 033 * unchanged. 034 */ 035public class XmlNavigateCommand extends AbstractStreamCommand { 036 private static final Logger LOGGER = LoggerFactory.getLogger(XmlNavigateCommand.class); 037 private static final XMLEventFactory EVENT_FACTORY = XMLEventFactory.newInstance(); 038 039 private TreePath treePath; 040 private IRule rule; 041 042 /** 043 * Constructor for XML navigation with TreePath selector and transformation rule. 044 * 045 * @param treePath the TreePath to select elements 046 * @param rule the transformation rule to apply to selected elements 047 * @throws IllegalArgumentException if treePath or rule is null 048 */ 049 private XmlNavigateCommand(TreePath treePath, IRule rule) { 050 this.treePath = treePath; 051 this.rule = rule; 052 } 053 054 /** 055 * Factory method for creating an XML navigation command with TreePath and rule. 056 * 057 * @param treePath the TreePath to select elements 058 * @param rule the transformation rule to apply to selected elements 059 * @return an XmlNavigateCommand that transforms the specified TreePath elements with the given 060 * rule 061 * @throws IllegalArgumentException if treePath or rule is null 062 */ 063 public static XmlNavigateCommand create(TreePath treePath, IRule rule) { 064 if (treePath == null) { 065 throw new IllegalArgumentException("TreePath cannot be null"); 066 } 067 if (rule == null) { 068 throw new IllegalArgumentException("Rule cannot be null"); 069 } 070 return new XmlNavigateCommand(treePath, rule); 071 } 072 073 @Override 074 public void execute(InputStream inputStream, OutputStream outputStream) throws IOException { 075 XMLEventReader eventReader = null; 076 XMLEventWriter eventWriter = null; 077 IOException primaryException = null; 078 079 try { 080 eventReader = createXMLEventReader(inputStream); 081 eventWriter = createXMLEventWriter(outputStream); 082 navigateXmlWithRule(eventReader, eventWriter, treePath, rule); 083 eventWriter.flush(); 084 } catch (XMLStreamException e) { 085 primaryException = buildXmlException(e); 086 throw primaryException; 087 } finally { 088 closeResources(eventReader, eventWriter, primaryException); 089 } 090 } 091 092 private void navigateXmlWithRule( 093 XMLEventReader eventReader, XMLEventWriter eventWriter, TreePath treePath, IRule rule) 094 throws XMLStreamException { 095 List<String> currentPath = new ArrayList<>(); 096 097 while (eventReader.hasNext()) { 098 XMLEvent event = eventReader.nextEvent(); 099 100 if (event.isStartElement()) { 101 currentPath.add(event.asStartElement().getName().getLocalPart()); 102 } else if (event.isEndElement()) { 103 if (!currentPath.isEmpty()) { 104 currentPath.remove(currentPath.size() - 1); 105 } else { 106 LOGGER.warn( 107 "Unexpected end element '{}' with empty path stack — possible malformed XML", 108 event.asEndElement().getName().getLocalPart()); 109 } 110 } else if (event.isCharacters() && treePath.matches(currentPath)) { 111 String data = event.asCharacters().getData(); 112 String transformed; 113 try { 114 transformed = rule.apply(data); 115 } catch (RuntimeException ruleEx) { 116 throw new XMLStreamException("Rule application failed at path " + currentPath, ruleEx); 117 } 118 // Write every event unconditionally; character events at the target path are replaced 119 // above. 120 event = EVENT_FACTORY.createCharacters(transformed); 121 } 122 123 eventWriter.add(event); 124 } 125 } 126 127 private XMLEventReader createXMLEventReader(InputStream inputStream) throws XMLStreamException { 128 XMLInputFactory inputFactory = SecureXmlConfiguration.createSecureXMLInputFactory(); 129 XMLEventReader reader = inputFactory.createXMLEventReader(inputStream); 130 if (!reader.hasNext()) { 131 try { 132 reader.close(); 133 } catch (XMLStreamException closeEx) { 134 LOGGER.warn("Failed to close empty XMLEventReader", closeEx); 135 } 136 throw new XMLStreamException("Empty XML input"); 137 } 138 return reader; 139 } 140 141 private XMLEventWriter createXMLEventWriter(OutputStream outputStream) throws XMLStreamException { 142 XMLOutputFactory outputFactory = XMLOutputFactory.newInstance(); 143 return outputFactory.createXMLEventWriter(outputStream); 144 } 145 146 private IOException buildXmlException(XMLStreamException e) { 147 String location = ""; 148 if (e.getLocation() != null) { 149 int line = e.getLocation().getLineNumber(); 150 int column = e.getLocation().getColumnNumber(); 151 if (line > 0 && column > 0) { 152 location = String.format(" at line %d, column %d", line, column); 153 } 154 } 155 LOGGER.error("XML processing failed{}", location, e); 156 return new IOException("XML processing failed" + location, e); 157 } 158 159 private void closeResources( 160 XMLEventReader eventReader, XMLEventWriter eventWriter, IOException primaryException) { 161 if (eventWriter != null) { 162 try { 163 eventWriter.flush(); 164 } catch (XMLStreamException e) { 165 LOGGER.warn("Failed to flush XMLEventWriter during cleanup", e); 166 } 167 try { 168 eventWriter.close(); 169 } catch (XMLStreamException e) { 170 LOGGER.warn("Failed to close XMLEventWriter", e); 171 } catch (RuntimeException e) { 172 if (primaryException != null) { 173 primaryException.addSuppressed(e); 174 } else { 175 throw e; 176 } 177 } 178 } 179 if (eventReader != null) { 180 try { 181 eventReader.close(); 182 } catch (XMLStreamException e) { 183 LOGGER.warn("Failed to close XMLEventReader", e); 184 } catch (RuntimeException e) { 185 if (primaryException != null) { 186 primaryException.addSuppressed(e); 187 } else { 188 throw e; 189 } 190 } 191 } 192 } 193}