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}