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