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.io.OutputStreamWriter; 011import java.io.Writer; 012import java.nio.charset.StandardCharsets; 013import java.util.ArrayList; 014import java.util.List; 015import java.util.logging.Logger; 016import javax.xml.stream.XMLEventFactory; 017import javax.xml.stream.XMLEventReader; 018import javax.xml.stream.XMLEventWriter; 019import javax.xml.stream.XMLInputFactory; 020import javax.xml.stream.XMLOutputFactory; 021import javax.xml.stream.XMLStreamException; 022import javax.xml.stream.events.XMLEvent; 023 024/** 025 * XML Navigate Command Class 026 * 027 * <p>This class implements command for targeted XML transformation using XPath. It identifies 028 * specific elements using XPath expressions and applies IRule transformations to those elements 029 * while preserving the overall XML structure. 030 */ 031public class XmlNavigateCommand extends AbstractStreamCommand { 032 private static final Logger LOGGER = Logger.getLogger(XmlNavigateCommand.class.getName()); 033 private static final XMLEventFactory EVENT_FACTORY = XMLEventFactory.newInstance(); 034 035 private TreePath treePath; 036 private IRule rule; 037 038 /** 039 * Constructor for XML navigation with TreePath selector and transformation rule. 040 * 041 * @param treePath the TreePath to select elements 042 * @param rule the transformation rule to apply to selected elements 043 * @throws IllegalArgumentException if treePath or rule is null 044 */ 045 public XmlNavigateCommand(TreePath treePath, IRule rule) { 046 if (treePath == null) { 047 throw new IllegalArgumentException("TreePath cannot be null"); 048 } 049 if (rule == null) { 050 throw new IllegalArgumentException("Rule cannot be null"); 051 } 052 this.treePath = treePath; 053 this.rule = rule; 054 } 055 056 /** 057 * Factory method for creating an XML navigation command with TreePath and rule. 058 * 059 * @param treePath the TreePath to select elements 060 * @param rule the transformation rule to apply to selected elements 061 * @return an XmlNavigateCommand that transforms the specified TreePath elements with the given 062 * rule 063 * @throws IllegalArgumentException if rule is null 064 */ 065 public static XmlNavigateCommand create(TreePath treePath, IRule rule) { 066 return new XmlNavigateCommand(treePath, rule); 067 } 068 069 @Override 070 protected String getCommandDetails() { 071 return String.format("XmlNavigateCommand(treePath='%s')", treePath.toString()); 072 } 073 074 @Override 075 protected void executeInternal(InputStream inputStream, OutputStream outputStream) 076 throws IOException { 077 try (Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) { 078 // Apply rule to specific XPath elements while preserving structure 079 applyRuleToXmlPath(inputStream, writer); 080 } catch (XMLStreamException e) { 081 throw new IOException("XML processing error", e); 082 } 083 } 084 085 /** 086 * Apply transformation rule to specific XPath elements while preserving XML structure This is a 087 * simplified implementation - production version would need proper XPath library 088 */ 089 private void applyRuleToXmlPath(InputStream inputStream, Writer writer) 090 throws IOException, XMLStreamException { 091 XMLEventReader eventReader = null; 092 XMLEventWriter eventWriter = null; 093 094 try { 095 eventReader = createXMLEventReader(inputStream); 096 eventWriter = createXMLEventWriter(writer); 097 navigateXmlWithRule(eventReader, eventWriter, treePath, rule); 098 } catch (XMLStreamException e) { 099 handleXmlException(e); 100 } finally { 101 closeResources(eventReader, eventWriter); 102 } 103 } 104 105 private void navigateXmlWithRule( 106 XMLEventReader eventReader, XMLEventWriter eventWriter, TreePath treePath, IRule rule) 107 throws XMLStreamException { 108 List<String> currentPath = new ArrayList<>(); 109 boolean inTargetElement = false; 110 int targetDepth = 0; 111 112 while (eventReader.hasNext()) { 113 XMLEvent event = eventReader.nextEvent(); 114 115 if (event.isStartElement()) { 116 String elementName = event.asStartElement().getName().getLocalPart(); 117 currentPath.add(elementName); 118 119 if (treePath.matches(currentPath)) { 120 inTargetElement = true; 121 targetDepth = currentPath.size(); 122 eventWriter.add(event); 123 } else if (inTargetElement) { 124 eventWriter.add(event); 125 } 126 } else if (event.isEndElement()) { 127 if (inTargetElement) { 128 eventWriter.add(event); 129 if (currentPath.size() == targetDepth) { 130 inTargetElement = false; 131 // Add newline as simple characters 132 eventWriter.add(EVENT_FACTORY.createCharacters("\n")); 133 } 134 } 135 currentPath.remove(currentPath.size() - 1); 136 } else if (event.isCharacters() && inTargetElement) { 137 // Apply rule to character data in target elements 138 String originalData = event.asCharacters().getData(); 139 String transformedData = rule.apply(originalData); 140 if (!originalData.equals(transformedData)) { 141 // Create new character event with transformed data 142 XMLEvent transformedEvent = EVENT_FACTORY.createCharacters(transformedData); 143 eventWriter.add(transformedEvent); 144 } else { 145 eventWriter.add(event); 146 } 147 } 148 } 149 } 150 151 // Common factory methods and utilities 152 private XMLEventReader createXMLEventReader(InputStream inputStream) throws XMLStreamException { 153 XMLInputFactory inputFactory = SecureXmlConfiguration.createSecureXMLInputFactory(); 154 XMLEventReader reader = inputFactory.createXMLEventReader(inputStream); 155 if (!reader.hasNext()) { 156 throw new XMLStreamException("Empty XML input"); 157 } 158 return reader; 159 } 160 161 private XMLEventWriter createXMLEventWriter(Writer writer) throws XMLStreamException { 162 XMLOutputFactory outputFactory = XMLOutputFactory.newInstance(); 163 return outputFactory.createXMLEventWriter(writer); 164 } 165 166 private void handleXmlException(XMLStreamException e) throws IOException { 167 String message = e.getMessage(); 168 if (message != null && (message.contains("unclosed") || message.contains("end"))) { 169 throw new IOException("Invalid XML format - unclosed tags", e); 170 } 171 throw new IOException("Invalid XML format", e); 172 } 173 174 @FunctionalInterface 175 private interface CharacterDataProcessor { 176 String process(XMLEvent event, String data); 177 } 178 179 private void closeResources(XMLEventReader eventReader, XMLEventWriter eventWriter) { 180 if (eventReader != null) { 181 try { 182 eventReader.close(); 183 } catch (XMLStreamException e) { 184 LOGGER.warning("Failed to close XMLEventReader: " + e.getMessage()); 185 } 186 } 187 if (eventWriter != null) { 188 try { 189 eventWriter.close(); 190 } catch (XMLStreamException e) { 191 LOGGER.warning("Failed to close XMLEventWriter: " + e.getMessage()); 192 } 193 } 194 } 195}