Skip to content

Commit e7b5822

Browse files
committed
feat(flow-translator): support custom entities as part of EipFlow translation requests
1 parent 4ddc4d2 commit e7b5822

File tree

14 files changed

+475
-13
lines changed

14 files changed

+475
-13
lines changed

flow-translator/flow-translator-lib/pom.xml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>org.codice.keip</groupId>
88
<artifactId>flow-translator-lib</artifactId>
9-
<version>0.2.0</version>
9+
<version>0.3.0</version>
1010

1111
<packaging>jar</packaging>
1212

@@ -48,7 +48,7 @@
4848
<dependency>
4949
<groupId>org.codice.keip.schemas</groupId>
5050
<artifactId>validation</artifactId>
51-
<version>0.2.0</version>
51+
<version>0.3.0</version>
5252
<scope>test</scope>
5353
</dependency>
5454

@@ -139,17 +139,17 @@
139139
<limit>
140140
<counter>BRANCH</counter>
141141
<value>COVEREDRATIO</value>
142-
<minimum>0.93</minimum>
142+
<minimum>0.96</minimum>
143143
</limit>
144144
<limit>
145145
<counter>COMPLEXITY</counter>
146146
<value>COVEREDRATIO</value>
147-
<minimum>0.93</minimum>
147+
<minimum>0.94</minimum>
148148
</limit>
149149
<limit>
150150
<counter>LINE</counter>
151151
<value>COVEREDRATIO</value>
152-
<minimum>0.96</minimum>
152+
<minimum>0.97</minimum>
153153
</limit>
154154
</limits>
155155
</rule>

flow-translator/flow-translator-lib/src/main/java/org/codice/keip/flow/FlowTranslator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public FlowTranslator(GraphTransformer graphTransformer) {
3030
*/
3131
public List<TransformationError> toXml(Flow flow, Writer outputXml) throws TransformerException {
3232
EipGraph graph = GuavaGraph.from(flow);
33-
return graphTransformer.toXml(graph, outputXml);
33+
return graphTransformer.toXml(graph, outputXml, flow.customEntities());
3434
}
3535

3636
/**
Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,21 @@
11
package org.codice.keip.flow.model;
22

3+
import java.util.Collections;
34
import java.util.List;
5+
import java.util.Map;
46

57
// TODO: Look into generating this class from the JSON schema.
68
// Should match the EipFlow schema defined at:
79
// <project-root>/keip-canvas/schemas/model/json/eipFlow.schema.json
8-
public record Flow(List<EipNode> nodes, List<FlowEdge> edges) {}
10+
public record Flow(List<EipNode> nodes, List<FlowEdge> edges, Map<String, String> customEntities) {
11+
public Flow(List<EipNode> nodes, List<FlowEdge> edges) {
12+
this(nodes, edges, Collections.emptyMap());
13+
}
14+
15+
public Map<String, String> customEntities() {
16+
if (customEntities == null) {
17+
return Collections.emptyMap();
18+
}
19+
return Collections.unmodifiableMap(customEntities);
20+
}
21+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package org.codice.keip.flow.xml;
2+
3+
import com.ctc.wstx.stax.WstxEventFactory;
4+
import java.io.StringReader;
5+
import java.util.ArrayDeque;
6+
import java.util.ArrayList;
7+
import java.util.Deque;
8+
import java.util.Iterator;
9+
import java.util.List;
10+
import java.util.Map;
11+
import java.util.Set;
12+
import javax.xml.stream.XMLEventFactory;
13+
import javax.xml.stream.XMLEventReader;
14+
import javax.xml.stream.XMLEventWriter;
15+
import javax.xml.stream.XMLInputFactory;
16+
import javax.xml.stream.XMLStreamException;
17+
import javax.xml.stream.events.Attribute;
18+
import javax.xml.stream.events.StartElement;
19+
import javax.xml.stream.events.XMLEvent;
20+
import javax.xml.transform.TransformerException;
21+
import org.codice.keip.flow.error.TransformationError;
22+
23+
/** Validates Custom Entity XML content and adds the required id attribute before writing content */
24+
final class CustomEntityTransformer {
25+
26+
// These events will not be processed and are excluded from output.
27+
private static final Set<Integer> IGNORED_EVENTS =
28+
Set.of(XMLEvent.START_DOCUMENT, XMLEvent.END_DOCUMENT);
29+
30+
private final XMLEventFactory eventFactory = WstxEventFactory.newFactory();
31+
private final XMLInputFactory inputFactory;
32+
33+
CustomEntityTransformer(XMLInputFactory inputFactory) {
34+
this.inputFactory = inputFactory;
35+
}
36+
37+
/**
38+
* Validates that the XML content for all custom entities is well-formed and an 'id' attribute to
39+
* the content's root element matching its key in the customEntities Map.
40+
*
41+
* @param customEntities user-defined map of entityId to content
42+
* @param writer where the transformed content XML will be written to
43+
* @return An empty list for a successful transformation, otherwise a non-empty list of {@link
44+
* TransformationError} is returned.
45+
*/
46+
List<TransformationError> apply(Map<String, String> customEntities, XMLEventWriter writer) {
47+
List<TransformationError> errors = new ArrayList<>();
48+
49+
customEntities.forEach(
50+
(id, xml) -> {
51+
try {
52+
Deque<XMLEvent> eventQueue = new ArrayDeque<>();
53+
boolean wasRootIdAttributeAdded = false;
54+
55+
XMLEventReader reader = inputFactory.createXMLEventReader(new StringReader(xml));
56+
while (reader.hasNext()) {
57+
XMLEvent event = reader.nextEvent();
58+
59+
if (IGNORED_EVENTS.contains(event.getEventType())) {
60+
continue;
61+
}
62+
63+
if (!wasRootIdAttributeAdded && event.isStartElement()) {
64+
event = addIdAttributeToEntityRoot(event.asStartElement(), id);
65+
wasRootIdAttributeAdded = true;
66+
}
67+
68+
eventQueue.addLast(event);
69+
}
70+
71+
// only writes events once all content for this entity is processed without exceptions
72+
for (XMLEvent e : eventQueue) {
73+
writer.add(e);
74+
}
75+
76+
} catch (XMLStreamException e) {
77+
errors.add(
78+
new TransformationError(
79+
String.format("custom entity [%s]", id),
80+
new TransformerException(e.getMessage())));
81+
}
82+
});
83+
84+
return errors;
85+
}
86+
87+
private XMLEvent addIdAttributeToEntityRoot(StartElement root, String entityId) {
88+
List<Attribute> updatedAttrs = new ArrayList<>();
89+
updatedAttrs.add(eventFactory.createAttribute("id", entityId));
90+
91+
Iterator<Attribute> attrs = root.getAttributes();
92+
while (attrs.hasNext()) {
93+
var attr = attrs.next();
94+
if (!"id".equals(attr.getName().getLocalPart())) {
95+
updatedAttrs.add(attr);
96+
}
97+
}
98+
return eventFactory.createStartElement(
99+
root.getName(), updatedAttrs.iterator(), root.getNamespaces());
100+
}
101+
}

flow-translator/flow-translator-lib/src/main/java/org/codice/keip/flow/xml/GraphTransformer.java

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import static javax.xml.XMLConstants.XML_NS_PREFIX;
44

55
import com.ctc.wstx.stax.WstxEventFactory;
6+
import com.ctc.wstx.stax.WstxInputFactory;
67
import com.ctc.wstx.stax.WstxOutputFactory;
78
import java.io.Writer;
89
import java.util.ArrayList;
910
import java.util.Collection;
11+
import java.util.Collections;
1012
import java.util.HashMap;
1113
import java.util.Iterator;
1214
import java.util.List;
@@ -19,6 +21,7 @@
1921
import javax.xml.namespace.QName;
2022
import javax.xml.stream.XMLEventFactory;
2123
import javax.xml.stream.XMLEventWriter;
24+
import javax.xml.stream.XMLInputFactory;
2225
import javax.xml.stream.XMLOutputFactory;
2326
import javax.xml.stream.XMLStreamException;
2427
import javax.xml.stream.events.Attribute;
@@ -44,13 +47,15 @@ public abstract class GraphTransformer {
4447
private final Set<String> reservedPrefixes = collectReservedPrefixes();
4548

4649
private final NodeTransformerFactory nodeTransformerFactory;
50+
private final CustomEntityTransformer customEntityTransformer;
4751
// maps an eipNamespace to a NamespaceSpec
4852
private final Map<String, NamespaceSpec> registeredNamespaces;
4953

5054
protected GraphTransformer(
5155
NodeTransformerFactory nodeTransformerFactory, Collection<NamespaceSpec> namespaceSpecs) {
5256
validatePrefixes(namespaceSpecs);
5357
this.nodeTransformerFactory = nodeTransformerFactory;
58+
this.customEntityTransformer = new CustomEntityTransformer(initializeXMLInputFactory());
5459
this.registeredNamespaces = new HashMap<>();
5560
this.registeredNamespaces.put(defaultNamespace().eipNamespace(), defaultNamespace());
5661
requiredNamespaces().forEach(s -> this.registeredNamespaces.put(s.eipNamespace(), s));
@@ -81,12 +86,14 @@ public final void registerNodeTransformer(EipId id, NodeTransformer transformer)
8186
*
8287
* @param graph input graph
8388
* @param output where the output XML will be written to
89+
* @param customEntities user-defined entities to be inlined in the output
8490
* @return An empty list for a successful transformation, otherwise a non-empty list of {@link
8591
* TransformationError} is returned.
8692
* @throws TransformerException thrown if a critical error preventing the transformation is
8793
* encountered
8894
*/
89-
public final List<TransformationError> toXml(EipGraph graph, Writer output)
95+
public final List<TransformationError> toXml(
96+
EipGraph graph, Writer output, Map<String, String> customEntities)
9097
throws TransformerException {
9198
List<TransformationError> errors = new ArrayList<>();
9299
try {
@@ -98,7 +105,9 @@ public final List<TransformationError> toXml(EipGraph graph, Writer output)
98105
StartElement root = createRootElement(graph);
99106
writer.add(root);
100107

101-
writeNodes(graph, writer, errors);
108+
errors.addAll(customEntityTransformer.apply(customEntities, writer));
109+
110+
errors.addAll(writeNodes(graph, writer));
102111

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

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

124+
/**
125+
* Transform an {@link EipGraph} instance to an XML document
126+
*
127+
* @param graph input graph
128+
* @param output where the output XML will be written to
129+
* @return An empty list for a successful transformation, otherwise a non-empty list of {@link
130+
* TransformationError} is returned.
131+
* @throws TransformerException thrown if a critical error preventing the transformation is
132+
* encountered
133+
*/
134+
public final List<TransformationError> toXml(EipGraph graph, Writer output)
135+
throws TransformerException {
136+
return toXml(graph, output, Collections.emptyMap());
137+
}
138+
115139
protected abstract NamespaceSpec defaultNamespace();
116140

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

199-
private void writeNodes(EipGraph graph, XMLEventWriter writer, List<TransformationError> errors) {
223+
private List<TransformationError> writeNodes(EipGraph graph, XMLEventWriter writer) {
224+
List<TransformationError> errors = new ArrayList<>();
225+
200226
// Using a for-each loop rather than stream operations due to the checked exception.
201227
// If this approach proves inefficient, an alternative is to define our own ErrorListener
202228
// interface that throws runtime exceptions.
@@ -210,6 +236,7 @@ private void writeNodes(EipGraph graph, XMLEventWriter writer, List<Transformati
210236
errors.add(error);
211237
}
212238
}
239+
return errors;
213240
}
214241

215242
private void writeElement(XmlElement element, XMLEventWriter writer) {
@@ -258,4 +285,11 @@ private Set<String> collectReservedPrefixes() {
258285
requiredPrefixes)
259286
.collect(Collectors.toUnmodifiableSet());
260287
}
288+
289+
static XMLInputFactory initializeXMLInputFactory() {
290+
XMLInputFactory factory = WstxInputFactory.newFactory();
291+
factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
292+
factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
293+
return factory;
294+
}
261295
}

flow-translator/flow-translator-lib/src/test/groovy/org/codice/keip/flow/FlowToSpringIntegrationTest.groovy

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class FlowToSpringIntegrationTest extends Specification {
4949
"flowGraph2.json" | Path.of("end-to-end", "spring-integration-2.xml").toString()
5050
"flowGraph3.json" | Path.of("end-to-end", "spring-integration-3.xml").toString()
5151
"flowGraph4.json" | Path.of("end-to-end", "spring-integration-4.xml").toString()
52+
"flowGraph5.json" | Path.of("end-to-end", "spring-integration-5.xml").toString()
5253
}
5354

5455
def "Verify transformation error list is populated on node transformation error"() {

0 commit comments

Comments
 (0)