Skip to content

Commit 15e62ea

Browse files
committed
[bugfix] Cyclic dependency import detection for XQuery 1.0 (i.e. XQST0093)
1 parent 4b92888 commit 15e62ea

File tree

3 files changed

+192
-3
lines changed

3 files changed

+192
-3
lines changed

exist-core/pom.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,17 @@
514514
<artifactId>ant</artifactId>
515515
</dependency>
516516

517+
<dependency>
518+
<groupId>org.jgrapht</groupId>
519+
<artifactId>jgrapht-core</artifactId>
520+
<version>1.5.2</version>
521+
</dependency>
522+
523+
<dependency>
524+
<groupId>org.jgrapht</groupId>
525+
<artifactId>jgrapht-opt</artifactId>
526+
<version>1.5.2</version>
527+
</dependency>
517528

518529
<dependency>
519530
<groupId>junit</groupId>

exist-core/src/main/java/org/exist/xquery/ModuleContext.java

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@
4848

4949

5050
/**
51-
* Subclass of {@link org.exist.xquery.XQueryContext} for
52-
* imported modules.
51+
* Subclass of {@link org.exist.xquery.XQueryContext} for imported modules.
5352
*
5453
* @author wolf
54+
* @author <a href="mailto:[email protected]">Adam Retter</a>
5555
*/
5656
public class ModuleContext extends XQueryContext {
5757

@@ -66,7 +66,8 @@ public ModuleContext(final XQueryContext parentContext, final String moduleNames
6666
super(parentContext != null ? parentContext.db : null,
6767
parentContext != null ? parentContext.getConfiguration() : null,
6868
null,
69-
false);
69+
false,
70+
null);
7071
this.moduleNamespace = moduleNamespace;
7172
this.modulePrefix = modulePrefix;
7273
this.location = location;
@@ -95,6 +96,52 @@ public void setModuleNamespace(final String prefix, final String namespaceURI) {
9596
this.moduleNamespace = namespaceURI;
9697
}
9798

99+
@Override
100+
protected void addModuleVertex(final ModuleVertex moduleVertex) {
101+
getRootContext().addModuleVertex(moduleVertex);
102+
}
103+
104+
protected boolean hasModuleVertex(final ModuleVertex moduleVertex) {
105+
return getRootContext().hasModuleVertex(moduleVertex);
106+
}
107+
108+
@Override
109+
protected void addModuleEdge(final ModuleVertex source, final ModuleVertex sink) {
110+
getRootContext().addModuleEdge(source, sink);
111+
}
112+
113+
@Override
114+
protected boolean hasModulePath(final ModuleVertex source, final ModuleVertex sink) {
115+
return getRootContext().hasModulePath(source, sink);
116+
}
117+
118+
@Override
119+
public @Nullable Module[] importModule(@Nullable String namespaceURI, @Nullable String prefix, @Nullable AnyURIValue[] locationHints) throws XPathException {
120+
final ModuleVertex thisModuleVertex = new ModuleVertex(moduleNamespace, location);
121+
122+
for (final AnyURIValue locationHint : locationHints) {
123+
final ModuleVertex imporedModuleVertex = new ModuleVertex(namespaceURI, locationHint.toString());
124+
125+
if (!hasModuleVertex(imporedModuleVertex)) {
126+
addModuleVertex(imporedModuleVertex);
127+
} else {
128+
// Check if there is already a path from the imported module to this module
129+
if (getXQueryVersion() == 10 && namespaceURI != null && locationHints != null && hasModulePath(imporedModuleVertex, thisModuleVertex)) {
130+
throw new XPathException(ErrorCodes.XQST0093, "Detected cyclic import between modules: " + getModuleNamespace() + " at: " + getLocation() + ", and: " + namespaceURI + " at: " + locationHint.toString());
131+
}
132+
}
133+
134+
if (!hasModuleVertex(thisModuleVertex)) {
135+
// NOTE(AR) may occur when the actual module has a different namespace from that of the `import module namespace`... will later raise an XQST0047 error
136+
addModuleVertex(thisModuleVertex);
137+
}
138+
139+
addModuleEdge(thisModuleVertex, imporedModuleVertex);
140+
}
141+
142+
return super.importModule(namespaceURI, prefix, locationHints);
143+
}
144+
98145
@Override
99146
protected @Nullable Module importModuleFromLocation(final String namespaceURI, @Nullable final String prefix, final AnyURIValue locationHint) throws XPathException {
100147
// guard against self-recursive import - see: https://github.com/eXist-db/exist/issues/3448

exist-core/src/main/java/org/exist/xquery/XQueryContext.java

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.nio.file.Paths;
3434
import java.util.*;
3535
import java.util.concurrent.CopyOnWriteArrayList;
36+
import java.util.concurrent.ThreadPoolExecutor;
3637
import java.util.concurrent.atomic.AtomicReference;
3738
import java.util.function.BiFunction;
3839
import java.util.function.Consumer;
@@ -94,6 +95,13 @@
9495
import org.exist.xquery.update.Modification;
9596
import org.exist.xquery.util.SerializerUtils;
9697
import org.exist.xquery.value.*;
98+
import org.jgrapht.alg.interfaces.ShortestPathAlgorithm;
99+
import org.jgrapht.alg.shortestpath.TransitNodeRoutingShortestPath;
100+
import org.jgrapht.graph.DefaultEdge;
101+
import org.jgrapht.graph.DefaultGraphType;
102+
import org.jgrapht.opt.graph.fastutil.FastutilMapGraph;
103+
import org.jgrapht.util.ConcurrencyUtil;
104+
import org.jgrapht.util.SupplierUtil;
97105
import org.w3c.dom.Node;
98106

99107
import static com.evolvedbinary.j8fu.OptionalUtil.or;
@@ -224,6 +232,11 @@ public class XQueryContext implements BinaryValueManager, Context {
224232
*/
225233
private Object2ObjectMap<String, Module[]> allModules = new Object2ObjectOpenHashMap<>();
226234

235+
/**
236+
* Describes a graph of all the modules and how they import each other.
237+
*/
238+
private @Nullable final FastutilMapGraph<ModuleVertex, DefaultEdge> modulesDependencyGraph;
239+
227240
/**
228241
* Used to save current state when modules are imported dynamically
229242
*/
@@ -454,6 +467,10 @@ public XQueryContext(@Nullable final Database db, @Nullable final Configuration
454467
}
455468

456469
protected XQueryContext(@Nullable final Database db, @Nullable final Configuration configuration, @Nullable final Profiler profiler, final boolean loadDefaults) {
470+
this(db, configuration, profiler, loadDefaults, new FastutilMapGraph<>(null, SupplierUtil.createDefaultEdgeSupplier(), DefaultGraphType.directedPseudograph().asUnweighted()));
471+
}
472+
473+
protected XQueryContext(@Nullable final Database db, @Nullable final Configuration configuration, @Nullable final Profiler profiler, final boolean loadDefaults, final @Nullable FastutilMapGraph<ModuleVertex, DefaultEdge> modulesDependencyGraph) {
457474
this.db = db;
458475

459476
// if needed, fallback to db.getConfiguration
@@ -474,6 +491,8 @@ protected XQueryContext(@Nullable final Database db, @Nullable final Configurati
474491
this.profiler = new Profiler(null);
475492
}
476493

494+
this.modulesDependencyGraph = modulesDependencyGraph;
495+
477496
this.watchdog = new XQueryWatchDog(this);
478497

479498
// load configuration defaults
@@ -1529,6 +1548,65 @@ public void addModule(final String namespaceURI, final Module module) {
15291548
addRootModule(namespaceURI, module);
15301549
}
15311550

1551+
/**
1552+
* Add a vertex to the Modules Dependency Graph.
1553+
*
1554+
* @param moduleVertex the module vertex
1555+
*/
1556+
protected void addModuleVertex(final ModuleVertex moduleVertex) {
1557+
modulesDependencyGraph.addVertex(moduleVertex);
1558+
}
1559+
1560+
/**
1561+
* Check if a vertex exists in the Modules Dependency Graph.
1562+
*
1563+
* @param moduleVertex the module vertex to look for
1564+
*
1565+
* @return true if the module vertex exists, false otherwise
1566+
*/
1567+
protected boolean hasModuleVertex(final ModuleVertex moduleVertex) {
1568+
return modulesDependencyGraph.containsVertex(moduleVertex);
1569+
}
1570+
1571+
/**
1572+
* Add an edge between two Modules in the Dependency Graph.
1573+
*
1574+
* @param source the importing module
1575+
* @param sink the imported module
1576+
*/
1577+
protected void addModuleEdge(final ModuleVertex source, final ModuleVertex sink) {
1578+
modulesDependencyGraph.addEdge(source, sink);
1579+
}
1580+
1581+
/**
1582+
* Look for a path between two Modules in the Dependency Graph.
1583+
*
1584+
* @param source the module to start searching from
1585+
* @param sink the destination module to attempt to reach
1586+
*
1587+
* @return true, if there is a path between the mdoules, false otherwise
1588+
*/
1589+
protected boolean hasModulePath(final ModuleVertex source, final ModuleVertex sink) {
1590+
if (modulesDependencyGraph == null) {
1591+
return false;
1592+
}
1593+
1594+
ThreadPoolExecutor executor = null;
1595+
try {
1596+
executor = ConcurrencyUtil.createThreadPoolExecutor(2);
1597+
final ShortestPathAlgorithm<ModuleVertex, DefaultEdge> spa = new TransitNodeRoutingShortestPath<>(modulesDependencyGraph, executor);
1598+
return spa.getPath(source, sink) != null;
1599+
} finally {
1600+
if (executor != null) {
1601+
try {
1602+
ConcurrencyUtil.shutdownExecutionService(executor);
1603+
} catch (final InterruptedException e) {
1604+
Thread.currentThread().interrupt();
1605+
}
1606+
}
1607+
}
1608+
}
1609+
15321610
protected void setRootModules(final String namespaceURI, @Nullable final Module[] modules) {
15331611
if (modules == null) {
15341612
allModules.remove(namespaceURI); // unbind the module
@@ -2641,6 +2719,11 @@ private ExternalModule compileOrBorrowModule(final String namespaceURI, final St
26412719
}
26422720

26432721
final ExternalModuleImpl modExternal = new ExternalModuleImpl(namespaceURI, prefix);
2722+
2723+
// NOTE(AR) this is needed to support cyclic imports in XQuery 3.1, see: https://github.com/eXist-db/exist/pull/4996
2724+
addModule(namespaceURI, modExternal);
2725+
addModuleVertex(new ModuleVertex(namespaceURI, location));
2726+
26442727
final XQueryContext modContext = new ModuleContext(this, namespaceURI, prefix, location);
26452728
modExternal.setContext(modContext);
26462729
final XQueryLexer lexer = new XQueryLexer(modContext, reader);
@@ -3547,4 +3630,52 @@ public String getStringValue() {
35473630

35483631
return sb.toString();
35493632
}
3633+
3634+
@Immutable
3635+
public static class ModuleVertex {
3636+
private final String namespaceURI;
3637+
private final String location;
3638+
3639+
public ModuleVertex(final String namespaceURI) {
3640+
this.namespaceURI = namespaceURI;
3641+
this.location = null;
3642+
}
3643+
3644+
public ModuleVertex(final String namespaceURI, final String location) {
3645+
this.namespaceURI = namespaceURI;
3646+
this.location = location;
3647+
}
3648+
3649+
@Override
3650+
public boolean equals(final Object o) {
3651+
if (this == o) {
3652+
return true;
3653+
}
3654+
3655+
if (o == null || getClass() != o.getClass()) {
3656+
return false;
3657+
}
3658+
3659+
final ModuleVertex that = (ModuleVertex) o;
3660+
if (!namespaceURI.equals(that.namespaceURI)) {
3661+
return false;
3662+
}
3663+
return location.equals(that.location);
3664+
}
3665+
3666+
@Override
3667+
public int hashCode() {
3668+
int result = namespaceURI.hashCode();
3669+
result = 31 * result + location.hashCode();
3670+
return result;
3671+
}
3672+
3673+
@Override
3674+
public String toString() {
3675+
return "Module{" +
3676+
"namespaceURI='" + namespaceURI + '\'' +
3677+
"location='" + location + '\'' +
3678+
'}';
3679+
}
3680+
}
35503681
}

0 commit comments

Comments
 (0)