Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions flow-translator/flow-translator-lib/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>org.codice.keip</groupId>
<artifactId>flow-translator-lib</artifactId>
<version>0.2.0</version>
<version>0.3.0</version>

<packaging>jar</packaging>

Expand Down Expand Up @@ -48,7 +48,7 @@
<dependency>
<groupId>org.codice.keip.schemas</groupId>
<artifactId>validation</artifactId>
<version>0.2.0</version>
<version>0.3.0</version>
<scope>test</scope>
</dependency>

Expand Down Expand Up @@ -139,17 +139,17 @@
<limit>
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>0.93</minimum>
<minimum>0.96</minimum>
</limit>
<limit>
<counter>COMPLEXITY</counter>
<value>COVEREDRATIO</value>
<minimum>0.93</minimum>
<minimum>0.94</minimum>
</limit>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.96</minimum>
<minimum>0.97</minimum>
</limit>
</limits>
</rule>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public FlowTranslator(GraphTransformer graphTransformer) {
*/
public List<TransformationError> toXml(Flow flow, Writer outputXml) throws TransformerException {
EipGraph graph = GuavaGraph.from(flow);
return graphTransformer.toXml(graph, outputXml);
return graphTransformer.toXml(graph, outputXml, flow.customEntities());
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
package org.codice.keip.flow.model;

import java.util.Collections;
import java.util.List;
import java.util.Map;

// TODO: Look into generating this class from the JSON schema.
// Should match the EipFlow schema defined at:
// <project-root>/keip-canvas/schemas/model/json/eipFlow.schema.json
public record Flow(List<EipNode> nodes, List<FlowEdge> edges) {}
public record Flow(List<EipNode> nodes, List<FlowEdge> edges, Map<String, String> customEntities) {
public Flow(List<EipNode> nodes, List<FlowEdge> edges) {
this(nodes, edges, Collections.emptyMap());
}

public Map<String, String> customEntities() {
if (customEntities == null) {
return Collections.emptyMap();
}
return Collections.unmodifiableMap(customEntities);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package org.codice.keip.flow.xml;

import com.ctc.wstx.stax.WstxEventFactory;
import java.io.StringReader;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import javax.xml.transform.TransformerException;
import org.codice.keip.flow.error.TransformationError;

/** Validates Custom Entity XML content and adds the required id attribute before writing content */
final class CustomEntityTransformer {

// These events will not be processed and are excluded from output.
private static final Set<Integer> IGNORED_EVENTS =
Set.of(XMLEvent.START_DOCUMENT, XMLEvent.END_DOCUMENT);

private final XMLEventFactory eventFactory = WstxEventFactory.newFactory();
private final XMLInputFactory inputFactory;

CustomEntityTransformer(XMLInputFactory inputFactory) {
this.inputFactory = inputFactory;
}

/**
* Validates that the XML content for all custom entities is well-formed and an 'id' attribute to
* the content's root element matching its key in the customEntities Map.
*
* @param customEntities user-defined map of entityId to content
* @param writer where the transformed content XML will be written to
* @return An empty list for a successful transformation, otherwise a non-empty list of {@link
* TransformationError} is returned.
*/
List<TransformationError> apply(Map<String, String> customEntities, XMLEventWriter writer) {
List<TransformationError> errors = new ArrayList<>();

customEntities.forEach(
(id, xml) -> {
try {
Deque<XMLEvent> eventQueue = new ArrayDeque<>();
boolean wasRootIdAttributeAdded = false;

XMLEventReader reader = inputFactory.createXMLEventReader(new StringReader(xml));
while (reader.hasNext()) {
XMLEvent event = reader.nextEvent();

if (IGNORED_EVENTS.contains(event.getEventType())) {
continue;
}

if (!wasRootIdAttributeAdded && event.isStartElement()) {
event = addIdAttributeToEntityRoot(event.asStartElement(), id);
wasRootIdAttributeAdded = true;
}

eventQueue.addLast(event);
}

// only writes events once all content for this entity is processed without exceptions
for (XMLEvent e : eventQueue) {
writer.add(e);
}

} catch (XMLStreamException e) {
errors.add(
new TransformationError(
String.format("custom entity [%s]", id),
new TransformerException(e.getMessage())));
}
});

return errors;
}

private XMLEvent addIdAttributeToEntityRoot(StartElement root, String entityId) {
List<Attribute> updatedAttrs = new ArrayList<>();
updatedAttrs.add(eventFactory.createAttribute("id", entityId));

Iterator<Attribute> attrs = root.getAttributes();
while (attrs.hasNext()) {
var attr = attrs.next();
if (!"id".equals(attr.getName().getLocalPart())) {
updatedAttrs.add(attr);
}
}
return eventFactory.createStartElement(
root.getName(), updatedAttrs.iterator(), root.getNamespaces());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import static javax.xml.XMLConstants.XML_NS_PREFIX;

import com.ctc.wstx.stax.WstxEventFactory;
import com.ctc.wstx.stax.WstxInputFactory;
import com.ctc.wstx.stax.WstxOutputFactory;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
Expand All @@ -19,6 +21,7 @@
import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
Expand All @@ -44,13 +47,15 @@ public abstract class GraphTransformer {
private final Set<String> reservedPrefixes = collectReservedPrefixes();

private final NodeTransformerFactory nodeTransformerFactory;
private final CustomEntityTransformer customEntityTransformer;
// maps an eipNamespace to a NamespaceSpec
private final Map<String, NamespaceSpec> registeredNamespaces;

protected GraphTransformer(
NodeTransformerFactory nodeTransformerFactory, Collection<NamespaceSpec> namespaceSpecs) {
validatePrefixes(namespaceSpecs);
this.nodeTransformerFactory = nodeTransformerFactory;
this.customEntityTransformer = new CustomEntityTransformer(initializeXMLInputFactory());
this.registeredNamespaces = new HashMap<>();
this.registeredNamespaces.put(defaultNamespace().eipNamespace(), defaultNamespace());
requiredNamespaces().forEach(s -> this.registeredNamespaces.put(s.eipNamespace(), s));
Expand Down Expand Up @@ -81,12 +86,14 @@ public final void registerNodeTransformer(EipId id, NodeTransformer transformer)
*
* @param graph input graph
* @param output where the output XML will be written to
* @param customEntities user-defined entities to be inlined in the output
* @return An empty list for a successful transformation, otherwise a non-empty list of {@link
* TransformationError} is returned.
* @throws TransformerException thrown if a critical error preventing the transformation is
* encountered
*/
public final List<TransformationError> toXml(EipGraph graph, Writer output)
public final List<TransformationError> toXml(
EipGraph graph, Writer output, Map<String, String> customEntities)
throws TransformerException {
List<TransformationError> errors = new ArrayList<>();
try {
Expand All @@ -98,7 +105,9 @@ public final List<TransformationError> toXml(EipGraph graph, Writer output)
StartElement root = createRootElement(graph);
writer.add(root);

writeNodes(graph, writer, errors);
errors.addAll(customEntityTransformer.apply(customEntities, writer));

errors.addAll(writeNodes(graph, writer));

writer.add(eventFactory.createEndElement(root.getName(), null));

Expand All @@ -112,6 +121,21 @@ public final List<TransformationError> toXml(EipGraph graph, Writer output)
return errors;
}

/**
* Transform an {@link EipGraph} instance to an XML document
*
* @param graph input graph
* @param output where the output XML will be written to
* @return An empty list for a successful transformation, otherwise a non-empty list of {@link
* TransformationError} is returned.
* @throws TransformerException thrown if a critical error preventing the transformation is
* encountered
*/
public final List<TransformationError> toXml(EipGraph graph, Writer output)
throws TransformerException {
return toXml(graph, output, Collections.emptyMap());
}

protected abstract NamespaceSpec defaultNamespace();

protected abstract Set<NamespaceSpec> requiredNamespaces();
Expand Down Expand Up @@ -196,7 +220,9 @@ private Iterator<Namespace> getRootNamespaces(List<String> eipNamespaces) {
.iterator();
}

private void writeNodes(EipGraph graph, XMLEventWriter writer, List<TransformationError> errors) {
private List<TransformationError> writeNodes(EipGraph graph, XMLEventWriter writer) {
List<TransformationError> errors = new ArrayList<>();

// Using a for-each loop rather than stream operations due to the checked exception.
// If this approach proves inefficient, an alternative is to define our own ErrorListener
// interface that throws runtime exceptions.
Expand All @@ -210,6 +236,7 @@ private void writeNodes(EipGraph graph, XMLEventWriter writer, List<Transformati
errors.add(error);
}
}
return errors;
}

private void writeElement(XmlElement element, XMLEventWriter writer) {
Expand Down Expand Up @@ -258,4 +285,11 @@ private Set<String> collectReservedPrefixes() {
requiredPrefixes)
.collect(Collectors.toUnmodifiableSet());
}

static XMLInputFactory initializeXMLInputFactory() {
XMLInputFactory factory = WstxInputFactory.newFactory();
factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
return factory;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class FlowToSpringIntegrationTest extends Specification {
"flowGraph2.json" | Path.of("end-to-end", "spring-integration-2.xml").toString()
"flowGraph3.json" | Path.of("end-to-end", "spring-integration-3.xml").toString()
"flowGraph4.json" | Path.of("end-to-end", "spring-integration-4.xml").toString()
"flowGraph5.json" | Path.of("end-to-end", "spring-integration-5.xml").toString()
}

def "Verify transformation error list is populated on node transformation error"() {
Expand Down
Loading