Skip to content

Commit 635c2b5

Browse files
committed
Improve the implementation of JavaXPathContextFactoryImpl
- Reuse EMF's support for serializing directly to DOM which directly supports maintaining the required mappings. - Add ExampleQueriesTestCase.testRelative() to test relative paths via a nested context. - Also verify that in ExampleQueriesTestCase.testMenuQuery the query `//.[@id='menu.1'` is behaving correctly relative to the target example model. #423
1 parent 32169ea commit 635c2b5

File tree

3 files changed

+134
-75
lines changed

3 files changed

+134
-75
lines changed

bundles/org.eclipse.e4.emf.xpath/META-INF/MANIFEST.MF

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ Bundle-SymbolicName: org.eclipse.e4.emf.xpath
55
Bundle-Version: 0.6.0.qualifier
66
Bundle-RequiredExecutionEnvironment: JavaSE-17
77
Require-Bundle: org.eclipse.emf.ecore;bundle-version="2.35.0",
8-
org.eclipse.core.runtime;bundle-version="3.29.0"
8+
org.eclipse.core.runtime;bundle-version="3.29.0",
9+
org.eclipse.emf.ecore.xmi;bundle-version="2.9.0"
910
Export-Package: org.eclipse.e4.emf.xpath
1011
Bundle-Vendor: %Bundle-Vendor
1112
Automatic-Module-Name: org.eclipse.e4.emf.xpath

bundles/org.eclipse.e4.emf.xpath/src/org/eclipse/e4/emf/xpath/internal/java/JavaXPathContextFactoryImpl.java

Lines changed: 103 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,27 @@
1414

1515
package org.eclipse.e4.emf.xpath.internal.java;
1616

17+
import java.io.StringWriter;
1718
import java.util.HashMap;
1819
import java.util.Iterator;
1920
import java.util.List;
2021
import java.util.Map;
22+
import java.util.Map.Entry;
2123
import java.util.Objects;
2224
import java.util.stream.Stream;
2325
import java.util.stream.StreamSupport;
2426

27+
import javax.xml.XMLConstants;
28+
import javax.xml.namespace.NamespaceContext;
2529
import javax.xml.namespace.QName;
2630
import javax.xml.parsers.DocumentBuilder;
2731
import javax.xml.parsers.DocumentBuilderFactory;
2832
import javax.xml.parsers.ParserConfigurationException;
33+
import javax.xml.transform.OutputKeys;
34+
import javax.xml.transform.Transformer;
35+
import javax.xml.transform.TransformerFactory;
36+
import javax.xml.transform.dom.DOMSource;
37+
import javax.xml.transform.stream.StreamResult;
2938
import javax.xml.xpath.XPath;
3039
import javax.xml.xpath.XPathExpressionException;
3140
import javax.xml.xpath.XPathFactory;
@@ -36,19 +45,23 @@
3645
import org.eclipse.e4.emf.xpath.XPathContext;
3746
import org.eclipse.e4.emf.xpath.XPathContextFactory;
3847
import org.eclipse.e4.emf.xpath.XPathNotFoundException;
39-
import org.eclipse.emf.ecore.EAttribute;
40-
import org.eclipse.emf.ecore.EClass;
4148
import org.eclipse.emf.ecore.EObject;
42-
import org.eclipse.emf.ecore.EStructuralFeature;
43-
import org.eclipse.emf.ecore.util.EcoreUtil;
49+
import org.eclipse.emf.ecore.EcorePackage;
50+
import org.eclipse.emf.ecore.xmi.XMLResource;
51+
import org.eclipse.emf.ecore.xmi.impl.DefaultDOMHandlerImpl;
52+
import org.eclipse.emf.ecore.xmi.impl.XMIHelperImpl;
53+
import org.eclipse.emf.ecore.xmi.impl.XMLSaveImpl;
4454
import org.w3c.dom.Attr;
4555
import org.w3c.dom.Document;
4656
import org.w3c.dom.Element;
57+
import org.w3c.dom.NamedNodeMap;
4758
import org.w3c.dom.Node;
4859
import org.w3c.dom.NodeList;
4960

5061
public class JavaXPathContextFactoryImpl<T> extends XPathContextFactory<T> {
5162

63+
private static final boolean DEBUG = false;
64+
5265
@Override
5366
public XPathContext newContext(T contextBean) {
5467
return newContext(null, contextBean);
@@ -59,39 +72,30 @@ public XPathContext newContext(XPathContext parentContext, T contextBean) {
5972
if (!(contextBean instanceof EObject rootObject)) {
6073
throw new IllegalArgumentException();
6174
}
75+
6276
if (parentContext != null) {
6377
EObjectContext parent = ((EObjectContext) parentContext);
64-
Element rootElement = parent.object2domProxy.get(contextBean);
78+
Element rootElement = parent.domMapper.getElement(contextBean);
6579
if (rootElement == null) {
6680
throw new IllegalArgumentException("Context bean is not from the same tree its parent context");
6781
}
68-
return new EObjectContext(rootElement, parent);
82+
return new EObjectContext(rootElement, parent.xpath.getNamespaceContext(), parent.domMapper);
6983
}
84+
7085
DocumentBuilder documentBuilder;
7186
try {
7287
documentBuilder = DocumentBuilderFactory.newDefaultInstance().newDocumentBuilder();
7388
} catch (ParserConfigurationException e) {
7489
throw new IllegalStateException(e);
7590
}
76-
Document proxyDoc = documentBuilder.newDocument();
77-
78-
Map<Node, EObject> domProxy2object = new HashMap<>();
79-
Map<EObject, Element> object2domProxy = new HashMap<>();
80-
// The xpath '/' actually referes to the document but it's also expected to
81-
// match the root application
82-
domProxy2object.put(proxyDoc, rootObject);
8391

84-
Element rootElement = createElement(rootObject, proxyDoc);
85-
object2domProxy.put(rootObject, rootElement);
86-
domProxy2object.put(rootElement, rootObject);
87-
88-
rootObject.eAllContents().forEachRemaining(eObject -> {
89-
Element parent = object2domProxy.get(eObject.eContainer());
90-
Element proxy = object2domProxy.computeIfAbsent(eObject, o -> createElement(o, parent));
91-
domProxy2object.put(proxy, eObject);
92-
});
93-
94-
return new EObjectContext(rootElement, domProxy2object, object2domProxy);
92+
Document document = documentBuilder.newDocument();
93+
DOMMapping domHandler = new DOMMapping();
94+
Element rootElement = createElement(rootObject, document, domHandler);
95+
if (DEBUG) {
96+
dump(document);
97+
}
98+
return new EObjectContext(rootElement, createNamespaceContext(rootElement), domHandler);
9599
}
96100

97101
private static class EObjectContext implements XPathContext {
@@ -100,25 +104,16 @@ private static class EObjectContext implements XPathContext {
100104

101105
private final XPath xpath;
102106
private final Element rootElement;
103-
private final Map<Node, EObject> domProxy2object;
104-
private final Map<EObject, Element> object2domProxy;
107+
private final DOMMapping domMapper;
105108

106-
private EObjectContext(Element rootElement, Map<Node, EObject> domProxy2object,
107-
Map<EObject, Element> object2domProxy) {
109+
private EObjectContext(Element rootElement, NamespaceContext namespaceContext, DOMMapping domMapper) {
108110
this.rootElement = rootElement;
109-
this.domProxy2object = Map.copyOf(domProxy2object);
110-
this.object2domProxy = Map.copyOf(object2domProxy);
111+
this.domMapper = domMapper;
111112
this.xpath = XPATH_FACTORY.newXPath();
113+
this.xpath.setNamespaceContext(namespaceContext);
112114
this.xpath.setXPathFunctionResolver(this::resolveEMFFunctions);
113115
}
114116

115-
private EObjectContext(Element rootElement, EObjectContext parentContext) {
116-
this.rootElement = rootElement;
117-
this.domProxy2object = Map.copyOf(parentContext.domProxy2object);
118-
this.object2domProxy = Map.copyOf(parentContext.object2domProxy);
119-
this.xpath = parentContext.xpath;
120-
}
121-
122117
@Override
123118
public <R> Stream<R> stream(String path, Class<R> resultType) {
124119
// See XPathResultType for generally supported result types
@@ -128,8 +123,6 @@ public <R> Stream<R> stream(String path, Class<R> resultType) {
128123
type = resultType;
129124
}
130125

131-
// Fix the different root and allow .[predicate] and ..[predicate] which is
132-
// actually not permitted in XPath-1
133126
String pathEnhanced = path;
134127
if (path.equals("/")) {
135128
pathEnhanced = "/" + rootElement.getTagName();
@@ -138,7 +131,11 @@ public <R> Stream<R> stream(String path, Class<R> resultType) {
138131
// match the root object
139132
pathEnhanced = "/" + rootElement.getTagName() + path;
140133
}
134+
135+
// Fix the different root and allow .[predicate] and ..[predicate] which is
136+
// actually not permitted in XPath-1
141137
pathEnhanced = pathEnhanced.replace("..[", "parent::node()[").replace(".[", "self::node()[");
138+
142139
Object result;
143140
try {
144141
result = xpath.evaluateExpression(pathEnhanced, rootElement, type);
@@ -153,7 +150,7 @@ public <R> Stream<R> stream(String path, Class<R> resultType) {
153150
}
154151
return StreamSupport.stream(pathNodes.spliterator(), false).map(node -> {
155152
if (node instanceof Element || node instanceof Document) {
156-
return domProxy2object.get(node);
153+
return (EObject) domMapper.getValue(node);
157154
} else if (node instanceof Attr attribute) {
158155
return attribute.getValue();
159156
}
@@ -179,11 +176,12 @@ public Object getValue(String xpath) {
179176
}
180177

181178
private XPathFunction resolveEMFFunctions(QName functionName, int arity) {
182-
if (arity == 1 && "ecore".equals(functionName.getNamespaceURI())
179+
if (arity == 1 && EcorePackage.eNS_URI.equals(functionName.getNamespaceURI())
183180
&& "eClassName".equals(functionName.getLocalPart())) {
184181
return args -> {
185182
Node item = getSingleNodeArgument(args);
186-
return EObjectContext.this.domProxy2object.get(item).eClass().getName();
183+
EObject eObject = (EObject) EObjectContext.this.domMapper.getValue(item);
184+
return eObject == null ? null : eObject.eClass().getName();
187185
};
188186
}
189187
return null;
@@ -200,37 +198,75 @@ private static Node getSingleNodeArgument(List<?> args) throws XPathFunctionExce
200198
}
201199
throw new XPathFunctionException("Not a single node list: " + args);
202200
}
201+
}
202+
203+
private static Element createElement(EObject eObject, Document document, DOMMapping domMapper) {
204+
new XMLSaveImpl(Map.of(), new XMIHelperImpl(), "UTF-8").save(null, document,
205+
Map.of(XMLResource.OPTION_ROOT_OBJECTS, List.of(eObject)), domMapper);
206+
return document.getDocumentElement();
207+
}
208+
209+
private static NamespaceContext createNamespaceContext(Element element) {
210+
element.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:ecore", EcorePackage.eNS_URI);
211+
return new NamespaceContext() {
212+
final Map<String, String> xmlnsPrefixMap = new HashMap<>();
213+
final Map<String, String> xmlnsPrefixMapInverse = new HashMap<>();
214+
215+
{
216+
NamedNodeMap attributes = element.getAttributes();
217+
for (int i = 0, length = attributes.getLength(); i < length; ++i) {
218+
Attr attribute = (Attr) attributes.item(i);
219+
if ("xmlns".equals(attribute.getPrefix())) {
220+
String prefix = attribute.getLocalName();
221+
String namespace = attribute.getValue();
222+
xmlnsPrefixMap.put(prefix, namespace);
223+
xmlnsPrefixMapInverse.put(namespace, prefix);
224+
}
225+
}
226+
}
227+
228+
@Override
229+
public String getNamespaceURI(String prefix) {
230+
return xmlnsPrefixMap.get(prefix);
231+
}
232+
233+
@Override
234+
public String getPrefix(String namespaceURI) {
235+
return xmlnsPrefixMapInverse.get(namespaceURI);
236+
}
203237

238+
@Override
239+
public Iterator<String> getPrefixes(String namespaceURI) {
240+
String prefix = getPrefix(namespaceURI);
241+
List<String> list = prefix == null ? List.of() : List.of(prefix);
242+
return list.iterator();
243+
}
244+
};
204245
}
205246

206-
private static Element createElement(EObject eObject, Node parent) {
207-
EStructuralFeature containingFeature = eObject.eContainingFeature();
208-
EStructuralFeature containmentFeature = eObject.eContainmentFeature();
209-
String className = eObject.eClass().getName();
210-
String qName = containingFeature != null ? containingFeature.getName() : className;
211-
212-
Document document = parent instanceof Document documentParent ? documentParent : parent.getOwnerDocument();
213-
Element element = document.createElement(qName);
214-
EClass eClass = eObject.eClass();
215-
for (EAttribute attribute : eClass.getEAllAttributes()) {
216-
// TODO: or check how lists could be serialized as attributes? CSV? With
217-
// leading/trailing square-bracket?
218-
if (!attribute.isMany() && attribute.getEAttributeType().isSerializable() && eObject.eIsSet(attribute)) {
219-
Object value = eObject.eGet(attribute);
220-
try {
221-
String stringValue = EcoreUtil.convertToString(attribute.getEAttributeType(), value);
222-
element.setAttribute(attribute.getName(), stringValue);
223-
} catch (Exception e) {
224-
// TODO: avoid the occurrence of exceptions
247+
private static class DOMMapping extends DefaultDOMHandlerImpl {
248+
public Element getElement(Object object) {
249+
for (Entry<Node, Object> entry : nodeToObject.entrySet()) {
250+
if (Objects.equals(entry.getValue(), object)) {
251+
return (Element) entry.getKey();
225252
}
226253
}
254+
return null;
227255
}
228-
// TODO: set the xsi:type? This requires to declare the EPackage nsURI as xml
229-
// namespace initially, but it's unsure to me if that's even necessary.
230-
// String name = eClass.getName();
231-
// childElement.setAttributeNS("http://www.w3.org/2001/XMLSchema-instance", "xsi:type", name);
256+
}
232257

233-
parent.appendChild(element);
234-
return element;
258+
private static void dump(Document document) {
259+
try {
260+
TransformerFactory transformerFactory = TransformerFactory.newInstance();
261+
Transformer transformer = transformerFactory.newTransformer();
262+
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
263+
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
264+
StringWriter out = new StringWriter();
265+
transformer.transform(new DOMSource(document), new StreamResult(out));
266+
out.close();
267+
System.out.print(out);
268+
} catch (Exception ex) {
269+
throw new RuntimeException(ex);
270+
}
235271
}
236272
}

tests/org.eclipse.e4.emf.xpath.test/src/org/eclipse/e4/emf/xpath/test/ExampleQueriesTestCase.java

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
import org.eclipse.e4.emf.xpath.test.model.xpathtest.Node;
3333
import org.eclipse.e4.emf.xpath.test.model.xpathtest.Root;
3434
import org.eclipse.e4.emf.xpath.test.model.xpathtest.XpathtestPackage;
35+
import org.eclipse.emf.common.util.EList;
36+
import org.eclipse.emf.common.util.TreeIterator;
3537
import org.eclipse.emf.common.util.URI;
3638
import org.eclipse.emf.ecore.EObject;
3739
import org.eclipse.emf.ecore.resource.Resource;
@@ -47,6 +49,7 @@ public class ExampleQueriesTestCase {
4749
private ResourceSet resourceSet;
4850
private XPathContext xpathContext;
4951
private Resource resource;
52+
private XPathContextFactory<EObject> xpathContextFactory;
5053

5154
@Before
5255
public void setUp() {
@@ -58,13 +61,14 @@ public void setUp() {
5861
resourceSet.getPackageRegistry().put(XpathtestPackage.eNS_URI, XpathtestPackage.eINSTANCE);
5962
URI uri = URI.createPlatformPluginURI("/org.eclipse.e4.emf.xpath.test/model/Test.xmi", true);
6063
resource = resourceSet.getResource(uri, true);
61-
XPathContextFactory<EObject> f = EcoreXPathContextFactory.newInstance();
62-
xpathContext = f.newContext(resource.getContents().get(0));
64+
xpathContextFactory = EcoreXPathContextFactory.newInstance();
65+
xpathContext = xpathContextFactory.newContext(resource.getContents().get(0));
6366
}
6467

6568
@After
6669
public void tearDown() {
6770
xpathContext = null;
71+
xpathContextFactory = null;
6872
resource.unload();
6973
resourceSet.getResources().remove(resource);
7074
}
@@ -94,7 +98,7 @@ public void testSimpleQuery() {
9498
assertThat(xpathContext.getValue("//.[@id='element2.2']")).isInstanceOf(Node.class);
9599
assertThat(xpathContext.getValue("//.[ecore:eClassName(.)='ExtendedNode']")).isInstanceOf(ExtendedNode.class);
96100

97-
assertNotNull(xpathContext.getValue("//.[ecore:eClassName(.)='ExtendedNode']", ExtendedNode.class));
101+
assertEquals(rootApplication, xpathContext.getValue("."));
98102
}
99103

100104
@Test
@@ -111,12 +115,30 @@ public void testMenuQuery() {
111115
assertThat(i.next()).isInstanceOf(Node.class);
112116
assertTrue(i.hasNext());
113117
assertThat(i.next()).isInstanceOf(Menu.class);
114-
// EMF model has a loop in it, it just goes back to the top
115-
// assertFalse(i.hasNext());
118+
119+
int expectedCount = 0;
120+
for (TreeIterator<EObject> eAllContents = ((Root) application).eAllContents(); eAllContents.hasNext();) {
121+
EObject eObject = eAllContents.next();
122+
if (eObject instanceof Menu menu && "menu".equals(menu.getId())) {
123+
++expectedCount;
124+
}
125+
}
116126

117127
List<Menu> list = xpathContext.stream("//.[@id='menu.1']", Menu.class).toList();
118-
// EMF model has a loop in it, it just goes back to the top
119-
assertEquals(1, list.size());// TODO: check this difference
128+
assertEquals(expectedCount, list.size());
120129
}
121130

131+
@Test
132+
public void testRelative() {
133+
EObject context = resource.getContents().get(0);
134+
EList<EObject> eContents = context.eContents();
135+
EObject firstElement = eContents.get(0);
136+
XPathContext nestedXpathContext = xpathContextFactory.newContext(xpathContext, firstElement);
137+
138+
List<Node> dotList = nestedXpathContext.stream(".", Node.class).toList();
139+
assertEquals(List.of(firstElement), dotList);
140+
141+
List<Node> followingSiblingsList = nestedXpathContext.stream("following-sibling::*", Node.class).toList();
142+
assertEquals(eContents.subList(1, eContents.size()), followingSiblingsList);
143+
}
122144
}

0 commit comments

Comments
 (0)