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}