diff --git a/.github/workflows/run-qt3.yml b/.github/workflows/run-qt3.yml index bd86df6b34..37d5bcd515 100644 --- a/.github/workflows/run-qt3.yml +++ b/.github/workflows/run-qt3.yml @@ -104,7 +104,7 @@ jobs: qt3-jsoniq: needs: [resolve-baseline, build] - uses: RumbleDB/rumble-test-suite/.github/workflows/qt3-suite.yml@master + uses: RumbleDB/rumble-test-suite/.github/workflows/qt3-suite.yml@AtHint with: tested_parser: jsoniq rumble_artifact_name: rumble-build @@ -112,7 +112,7 @@ jobs: qt3-xquery: needs: [resolve-baseline, build] - uses: RumbleDB/rumble-test-suite/.github/workflows/qt3-suite.yml@master + uses: RumbleDB/rumble-test-suite/.github/workflows/qt3-suite.yml@AtHint with: tested_parser: xquery rumble_artifact_name: rumble-build diff --git a/src/main/java/org/rumbledb/compiler/DynamicContextVisitor.java b/src/main/java/org/rumbledb/compiler/DynamicContextVisitor.java index 824d8b52f4..dae1eedd62 100644 --- a/src/main/java/org/rumbledb/compiler/DynamicContextVisitor.java +++ b/src/main/java/org/rumbledb/compiler/DynamicContextVisitor.java @@ -357,6 +357,7 @@ public DynamicContext visitLibraryModule(LibraryModule module, DynamicContext ar if (!this.importedModuleContexts.containsKey(module.getNamespace())) { DynamicContext newContext = new DynamicContext(this.configuration); newContext.setNamedFunctions(argument.getNamedFunctions()); + this.importedModuleContexts.put(module.getNamespace(), newContext); DynamicContext importedContext = visitDescendants(module, newContext); this.importedModuleContexts.put(module.getNamespace(), importedContext); } diff --git a/src/main/java/org/rumbledb/compiler/ModuleImportLoader.java b/src/main/java/org/rumbledb/compiler/ModuleImportLoader.java index cb77c1ec35..4b98dcf07b 100644 --- a/src/main/java/org/rumbledb/compiler/ModuleImportLoader.java +++ b/src/main/java/org/rumbledb/compiler/ModuleImportLoader.java @@ -34,24 +34,31 @@ public static LibraryModule load( ExceptionMetadata metadata ) { URI baseURI = importingModuleContext.getStaticBaseURI(); - URI namespaceURI = URILiteralUtils.resolve(baseURI, namespace, metadata); - List candidates = locationHints.isEmpty() ? List.of(namespace) : locationHints; + String normalizedNamespace = URILiteralUtils.normalizeAsAnyURI(namespace); + List candidates = locationHints.isEmpty() ? List.of(normalizedNamespace) : locationHints; Exception lastFailure = null; for (String candidate : candidates) { - URI location = URILiteralUtils.resolve(baseURI, candidate, metadata); + URI location; + try { + location = URILiteralUtils.resolve(baseURI, candidate, metadata); + } catch (RumbleException e) { + lastFailure = e; + continue; + } try { LibraryModule module = VisitorHelpers.parseLibraryModuleFromLocation( location, + namespaceURI.toString(), importingModuleContext, compilationConfiguration, metadata ); - if (!namespaceURI.toString().equals(module.getNamespace())) { + if (!normalizedNamespace.equals(module.getNamespace())) { throw new ModuleNotFoundException( "A module with namespace " - + namespaceURI + + normalizedNamespace + " was not found. The namespace of the module at this location was: " + module.getNamespace(), metadata @@ -66,7 +73,7 @@ public static LibraryModule load( RumbleException exception = new ModuleNotFoundException( "Module not found: %s, cause: %s".formatted( - namespaceURI, + normalizedNamespace, lastFailure != null ? lastFailure.getMessage() : "unknown" ), metadata diff --git a/src/main/java/org/rumbledb/compiler/StaticContextVisitor.java b/src/main/java/org/rumbledb/compiler/StaticContextVisitor.java index 137ad258f0..51d4b1e147 100644 --- a/src/main/java/org/rumbledb/compiler/StaticContextVisitor.java +++ b/src/main/java/org/rumbledb/compiler/StaticContextVisitor.java @@ -137,8 +137,8 @@ public StaticContext visitMainModule(MainModule mainModule, StaticContext argume public StaticContext visitLibraryModule(LibraryModule libraryModule, StaticContext argument) { if (!this.importedModuleContexts.containsKey(libraryModule.getNamespace())) { StaticContext moduleContext = libraryModule.getStaticContext(); - this.visit(libraryModule.getProlog(), moduleContext); this.importedModuleContexts.put(libraryModule.getNamespace(), moduleContext); + this.visit(libraryModule.getProlog(), moduleContext); } argument.importModuleContext( this.importedModuleContexts.get(libraryModule.getNamespace()) diff --git a/src/main/java/org/rumbledb/compiler/TranslationVisitor.java b/src/main/java/org/rumbledb/compiler/TranslationVisitor.java index 76efac56f7..6f1166b18d 100644 --- a/src/main/java/org/rumbledb/compiler/TranslationVisitor.java +++ b/src/main/java/org/rumbledb/compiler/TranslationVisitor.java @@ -371,24 +371,19 @@ public Node visitProgram(JsoniqParser.ProgramContext ctx) { @Override public Node visitLibraryModule(JsoniqParser.LibraryModuleContext ctx) { String prefix = ctx.ncName().getText(); - String namespace = processURILiteral(ctx.uriLiteral()); + String namespace = URILiteralUtils.normalizeAsAnyURI(processURILiteral(ctx.uriLiteral())); if (namespace.equals("")) { throw new EmptyModuleURIException("Module URI is empty.", createMetadataFromContext(ctx)); } - URI resolvedURI = URILiteralUtils.resolve( - this.moduleContext.getStaticBaseURI(), - namespace, - createMetadataFromContext(ctx) - ); - this.libraryModuleNamespace = resolvedURI.toString(); + this.libraryModuleNamespace = namespace; bindNamespace( prefix, - resolvedURI.toString(), + namespace, createMetadataFromContext(ctx) ); Prolog prolog = (Prolog) this.visitProlog(ctx.prolog()); - LibraryModule module = new LibraryModule(prolog, resolvedURI.toString(), createMetadataFromContext(ctx)); + LibraryModule module = new LibraryModule(prolog, namespace, createMetadataFromContext(ctx)); module.setStaticContext(this.moduleContext); return module; } @@ -480,7 +475,7 @@ public Node visitProlog(JsoniqParser.PrologContext ctx) { uriString, createMetadataFromContext(setterContext.baseURIDecl()) ); - this.moduleContext.setStaticBaseUri(uri); + this.moduleContext.setStaticBaseUri(uri, URILiteralUtils.normalizeAsAnyURI(uriString)); baseURISet = true; continue; } @@ -4062,8 +4057,8 @@ private String resolveCollationUri(UriLiteralContext ctx) { } public LibraryModule processModuleImport(JsoniqParser.ModuleImportContext ctx) { - String namespace = processURILiteral(ctx.targetNamespace); ExceptionMetadata metadata = createMetadataFromContext(ctx); + String namespace = processURILiteral(ctx.targetNamespace); if (namespace.isEmpty()) { throw new EmptyModuleURIException("Module URI is empty.", metadata); } @@ -4076,8 +4071,10 @@ public LibraryModule processModuleImport(JsoniqParser.ModuleImportContext ctx) { ); } } + namespace = URILiteralUtils.normalizeAsAnyURI(namespace); List locationHints = ctx.locations.stream() .map(this::processURILiteral) + .map(URILiteralUtils::normalizeAsAnyURI) .collect(Collectors.toList()); LibraryModule libraryModule = ModuleImportLoader.load( namespace, diff --git a/src/main/java/org/rumbledb/compiler/VariableDependenciesVisitor.java b/src/main/java/org/rumbledb/compiler/VariableDependenciesVisitor.java index 11daaaf642..de1aa75a31 100644 --- a/src/main/java/org/rumbledb/compiler/VariableDependenciesVisitor.java +++ b/src/main/java/org/rumbledb/compiler/VariableDependenciesVisitor.java @@ -25,6 +25,8 @@ import org.rumbledb.config.RumbleRuntimeConfiguration; import org.rumbledb.context.Name; import org.rumbledb.exceptions.CycleInVariableDeclarationsException; +import org.rumbledb.exceptions.DuplicateFunctionIdentifierException; +import org.rumbledb.exceptions.ModuleDependencyCycleException; import org.rumbledb.exceptions.OurBadException; import org.rumbledb.exceptions.VariableAlreadyExistsException; import org.rumbledb.expressions.AbstractNodeVisitor; @@ -44,6 +46,7 @@ import org.rumbledb.expressions.flowr.WhereClause; import org.rumbledb.expressions.flowr.WindowClause; import org.rumbledb.expressions.module.FunctionDeclaration; +import org.rumbledb.expressions.module.LibraryModule; import org.rumbledb.expressions.module.Prolog; import org.rumbledb.expressions.module.TypeDeclaration; import org.rumbledb.expressions.module.VariableDeclaration; @@ -68,6 +71,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -555,6 +559,28 @@ public Void visitProlog(Prolog prolog, Void argument) { return null; } + @Override + public Void visitMainModule(org.rumbledb.expressions.module.MainModule mainModule, Void argument) { + visit(mainModule.getProlog(), null); + for (LibraryModule importedModule : mainModule.getProlog().getImportedModules()) { + visit(importedModule, null); + } + visit(mainModule.getProgram(), null); + return null; + } + + @Override + public Void visitLibraryModule(LibraryModule libraryModule, Void argument) { + visitImportedModules(libraryModule.getProlog().getImportedModules()); + visit(libraryModule.getProlog(), null); + if (isXQuery10Like()) { + validateModuleDependencyCycles(libraryModule); + } else { + validateDynamicContextCycles(libraryModule); + } + return null; + } + @Override public Void visitVariableDeclaration(VariableDeclaration expression, Void argument) { if (expression.getExpression() != null) { @@ -565,6 +591,154 @@ public Void visitVariableDeclaration(VariableDeclaration expression, Void argume return null; } + private void visitImportedModules(List importedModules) { + for (LibraryModule importedModule : importedModules) { + visit(importedModule, null); + } + } + + private boolean isXQuery10Like() { + String queryLanguage = this.rumbleRuntimeConfiguration.getQueryLanguage(); + return "xquery10".equals(queryLanguage) || "jsoniq10".equals(queryLanguage); + } + + private void validateModuleDependencyCycles(LibraryModule rootModule) { + DirectedAcyclicGraph dependencyGraph = new DirectedAcyclicGraph<>( + DefaultEdge.class + ); + addModuleDependencyEdges( + rootModule, + dependencyGraph, + Collections.newSetFromMap(new IdentityHashMap<>()) + ); + } + + private void addModuleDependencyEdges( + LibraryModule module, + DirectedAcyclicGraph dependencyGraph, + Set visitedModules + ) { + if (!visitedModules.add(module)) { + return; + } + dependencyGraph.addVertex(module); + Prolog prolog = module.getProlog(); + for (LibraryModule importedModule : prolog.getImportedModules()) { + addModuleDependencyEdges(importedModule, dependencyGraph, visitedModules); + if (dependsDirectlyOnModule(module, importedModule)) { + dependencyGraph.addVertex(importedModule); + try { + dependencyGraph.addEdge(importedModule, module); + } catch (IllegalArgumentException e) { + throw new ModuleDependencyCycleException( + "There is a cycle in the direct dependencies between imported modules.", + module.getMetadata() + ); + } + } + } + } + + private boolean dependsDirectlyOnModule(LibraryModule module, LibraryModule importedModule) { + Set importedNames = collectDirectDeclarationNames(importedModule.getProlog()); + for (VariableDeclaration variableDeclaration : module.getProlog().getVariableDeclarations()) { + if (!Collections.disjoint(getInputVariableDependencies(variableDeclaration), importedNames)) { + return true; + } + } + for (FunctionDeclaration functionDeclaration : module.getProlog().getFunctionDeclarations()) { + if (!Collections.disjoint(getInputVariableDependencies(functionDeclaration), importedNames)) { + return true; + } + } + return false; + } + + private Set collectDirectDeclarationNames(Prolog prolog) { + Set names = new TreeSet<>(); + for (VariableDeclaration variableDeclaration : prolog.getVariableDeclarations()) { + names.add(variableDeclaration.getVariableName()); + } + for (FunctionDeclaration functionDeclaration : prolog.getFunctionDeclarations()) { + names.add(functionDeclaration.getFunctionIdentifier().getNameWithArity()); + } + for (TypeDeclaration typeDeclaration : prolog.getTypeDeclarations()) { + names.add(typeDeclaration.getDefinition().getName()); + } + return names; + } + + private void validateDynamicContextCycles(LibraryModule rootModule) { + Map reachableDeclarations = new TreeMap<>(); + collectReachableDeclarations( + rootModule, + reachableDeclarations, + Collections.newSetFromMap(new IdentityHashMap<>()) + ); + DirectedAcyclicGraph dependencyGraph = new DirectedAcyclicGraph<>(DefaultEdge.class); + for (Node declaration : reachableDeclarations.values()) { + dependencyGraph.addVertex(declaration); + } + for (Node declaration : reachableDeclarations.values()) { + if (!(declaration instanceof VariableDeclaration) && !(declaration instanceof FunctionDeclaration)) { + continue; + } + for (Name name : getInputVariableDependencies(declaration)) { + Node dependency = reachableDeclarations.get(name); + if (dependency == null) { + continue; + } + if (dependency instanceof FunctionDeclaration && declaration instanceof FunctionDeclaration) { + continue; + } + try { + dependencyGraph.addEdge(dependency, declaration); + } catch (IllegalArgumentException e) { + throw new CycleInVariableDeclarationsException( + "There is a cycle in the dependencies in the variable and function declarations. It is thus impossible to build the dynamic context.", + declaration.getMetadata() + ); + } + } + } + } + + private void collectReachableDeclarations( + LibraryModule module, + Map nameToNodeMap, + Set visitedModules + ) { + if (!visitedModules.add(module)) { + return; + } + Prolog prolog = module.getProlog(); + for (LibraryModule importedModule : prolog.getImportedModules()) { + collectReachableDeclarations(importedModule, nameToNodeMap, visitedModules); + } + for (VariableDeclaration variableDeclaration : prolog.getVariableDeclarations()) { + Node previous = nameToNodeMap.putIfAbsent(variableDeclaration.getVariableName(), variableDeclaration); + if (previous != null && previous != variableDeclaration) { + throw new VariableAlreadyExistsException( + variableDeclaration.getVariableName(), + variableDeclaration.getMetadata() + ); + } + } + for (FunctionDeclaration functionDeclaration : prolog.getFunctionDeclarations()) { + Name key = functionDeclaration.getFunctionIdentifier().getNameWithArity(); + Node previous = nameToNodeMap.putIfAbsent(key, functionDeclaration); + if (previous != null && previous != functionDeclaration) { + throw new DuplicateFunctionIdentifierException( + functionDeclaration.getFunctionIdentifier(), + functionDeclaration.getMetadata() + ); + } + } + for (TypeDeclaration typeDeclaration : prolog.getTypeDeclarations()) { + nameToNodeMap.putIfAbsent(typeDeclaration.getDefinition().getName(), typeDeclaration); + } + } + @Override public Void visitFunctionDeclaration(FunctionDeclaration expression, Void argument) { visit(expression.getExpression(), null); diff --git a/src/main/java/org/rumbledb/compiler/VisitorHelpers.java b/src/main/java/org/rumbledb/compiler/VisitorHelpers.java index d175d569f7..868b91e1d7 100644 --- a/src/main/java/org/rumbledb/compiler/VisitorHelpers.java +++ b/src/main/java/org/rumbledb/compiler/VisitorHelpers.java @@ -23,6 +23,7 @@ import org.rumbledb.expressions.module.LibraryModule; import org.rumbledb.expressions.module.MainModule; import org.rumbledb.expressions.module.Module; +import org.rumbledb.expressions.module.Prolog; import org.rumbledb.parser.jsoniq.JsoniqLexer; import org.rumbledb.parser.jsoniq.JsoniqParser; import org.rumbledb.parser.xquery.XQueryLexer; @@ -213,18 +214,35 @@ public static MainModule parseMainModuleFromLocation( static LibraryModule parseLibraryModuleFromLocation( URI location, + String expectedNamespace, StaticContext importingModuleContext, CompilationConfiguration compilationConfiguration, ExceptionMetadata metadata ) throws IOException { + LibraryModule cachedModule = compilationConfiguration.getLibraryModule(location); + if (cachedModule != null) { + return cachedModule; + } + LibraryModule placeholder = createPlaceholderLibraryModule(expectedNamespace, location, importingModuleContext); + LibraryModule installedModule = compilationConfiguration.cacheLibraryModule(location, placeholder); + if (installedModule != placeholder) { + return installedModule; + } ModuleSource source = readModuleSource(location, compilationConfiguration, metadata); - return parseLibraryModule( - source.query(), - source.systemId(), - importingModuleContext, - compilationConfiguration - ); + try { + LibraryModule parsedModule = parseLibraryModule( + source.query(), + source.systemId(), + importingModuleContext, + compilationConfiguration + ); + populatePlaceholderLibraryModule(placeholder, parsedModule); + return placeholder; + } catch (RuntimeException e) { + compilationConfiguration.removeLibraryModule(location, placeholder); + throw e; + } } public static MainModule parseMainModuleFromQuery(String query, RumbleRuntimeConfiguration configuration) { @@ -539,6 +557,38 @@ private static LibraryModule parseXQueryLibraryModule( } } + private static LibraryModule createPlaceholderLibraryModule( + String expectedNamespace, + URI location, + StaticContext importingModuleContext + ) { + Prolog prolog = new Prolog( + new ArrayList<>(), + new ArrayList<>(), + new ArrayList<>(), + ExceptionMetadata.EMPTY_METADATA + ); + LibraryModule placeholder = new LibraryModule(prolog, expectedNamespace, ExceptionMetadata.EMPTY_METADATA); + StaticContext moduleContext = new StaticContext( + location, + importingModuleContext.getRumbleConfiguration() + ); + moduleContext.setUserDefinedFunctionsExecutionModes( + importingModuleContext.getUserDefinedFunctionsExecutionModes() + ); + placeholder.setStaticContext(moduleContext); + return placeholder; + } + + private static void populatePlaceholderLibraryModule(LibraryModule placeholder, LibraryModule parsedModule) { + placeholder.setNamespace(parsedModule.getNamespace()); + placeholder.setStaticContext(parsedModule.getStaticContext()); + Prolog targetProlog = placeholder.getProlog(); + targetProlog.getImportedModules().clear(); + targetProlog.getImportedModules().addAll(parsedModule.getProlog().getImportedModules()); + targetProlog.setDeclarations(new ArrayList<>(parsedModule.getProlog().getDeclarations())); + } + private static void populateExecutionModes(Module module, RumbleRuntimeConfiguration conf) { if (conf.debug()) { printTree(module, conf); diff --git a/src/main/java/org/rumbledb/compiler/XQueryTranslationVisitor.java b/src/main/java/org/rumbledb/compiler/XQueryTranslationVisitor.java index dbec6fba30..3c529b6833 100644 --- a/src/main/java/org/rumbledb/compiler/XQueryTranslationVisitor.java +++ b/src/main/java/org/rumbledb/compiler/XQueryTranslationVisitor.java @@ -355,24 +355,19 @@ public Node visitProgram(XQueryParser.ProgramContext ctx) { @Override public Node visitLibraryModule(XQueryParser.LibraryModuleContext ctx) { String prefix = ctx.ncName().getText(); - String namespace = processURILiteral(ctx.uriLiteral()); + String namespace = URILiteralUtils.normalizeAsAnyURI(processURILiteral(ctx.uriLiteral())); if (namespace.equals("")) { throw new EmptyModuleURIException("Module URI is empty.", createMetadataFromContext(ctx)); } - URI resolvedURI = URILiteralUtils.resolve( - this.moduleContext.getStaticBaseURI(), - namespace, - createMetadataFromContext(ctx) - ); - this.libraryModuleNamespace = resolvedURI.toString(); + this.libraryModuleNamespace = namespace; bindNamespace( prefix, - resolvedURI.toString(), + namespace, createMetadataFromContext(ctx) ); Prolog prolog = (Prolog) this.visitProlog(ctx.prolog()); - LibraryModule module = new LibraryModule(prolog, resolvedURI.toString(), createMetadataFromContext(ctx)); + LibraryModule module = new LibraryModule(prolog, namespace, createMetadataFromContext(ctx)); module.setStaticContext(this.moduleContext); return module; } @@ -574,7 +569,7 @@ private void processPrologPhase1Setter(SetterContext setterContext, PrologPhase1 uriString, createMetadataFromContext(setterContext.baseURIDecl()) ); - this.moduleContext.setStaticBaseUri(uri); + this.moduleContext.setStaticBaseUri(uri, URILiteralUtils.normalizeAsAnyURI(uriString)); flags.baseURISet = true; return; } @@ -3754,8 +3749,8 @@ private String resolveCollationUri(UriLiteralContext ctx) { } public LibraryModule processModuleImport(XQueryParser.ModuleImportContext ctx) { - String namespace = processURILiteral(ctx.targetNamespace); ExceptionMetadata metadata = createMetadataFromContext(ctx); + String namespace = processURILiteral(ctx.targetNamespace); if (namespace.isEmpty()) { throw new EmptyModuleURIException("Module URI is empty.", metadata); } @@ -3768,8 +3763,10 @@ public LibraryModule processModuleImport(XQueryParser.ModuleImportContext ctx) { ); } } + namespace = URILiteralUtils.normalizeAsAnyURI(namespace); List locationHints = ctx.locations.stream() .map(this::processURILiteral) + .map(URILiteralUtils::normalizeAsAnyURI) .collect(Collectors.toList()); LibraryModule libraryModule = ModuleImportLoader.load( namespace, diff --git a/src/main/java/org/rumbledb/compiler/utils/URILiteralUtils.java b/src/main/java/org/rumbledb/compiler/utils/URILiteralUtils.java index 7bf2467cc0..3e5396fa68 100644 --- a/src/main/java/org/rumbledb/compiler/utils/URILiteralUtils.java +++ b/src/main/java/org/rumbledb/compiler/utils/URILiteralUtils.java @@ -16,15 +16,22 @@ public final class URILiteralUtils { + private static final String XML_WHITESPACE_SEQUENCE = "[\\t\\n\\r ]+"; + private URILiteralUtils() { } + public static String normalizeAsAnyURI(String literal) { + return literal.replaceAll(XML_WHITESPACE_SEQUENCE, " ").trim(); + } + public static URI resolve(URI baseURI, String literal, ExceptionMetadata metadata) { + String normalizedLiteral = normalizeAsAnyURI(literal); try { - return FileSystemUtil.resolveURI(baseURI, literal, metadata); + return FileSystemUtil.resolveURI(baseURI, normalizedLiteral, metadata); } catch (CannotRetrieveResourceException exception) { InvalidURILiteralException result = new InvalidURILiteralException( - "Invalid URI literal: " + literal, + "Invalid URI literal: " + normalizedLiteral, metadata ); result.initCause(exception); diff --git a/src/main/java/org/rumbledb/config/CompilationConfiguration.java b/src/main/java/org/rumbledb/config/CompilationConfiguration.java index 1e2f7c141e..b8af66cb24 100644 --- a/src/main/java/org/rumbledb/config/CompilationConfiguration.java +++ b/src/main/java/org/rumbledb/config/CompilationConfiguration.java @@ -1,27 +1,60 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0. - */ - package org.rumbledb.config; +import org.rumbledb.expressions.module.LibraryModule; import org.rumbledb.resources.ResourceResolver; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; /** Configuration and resource resolution services used during compilation. */ -public record CompilationConfiguration( - RumbleRuntimeConfiguration runtimeConfiguration, - ResourceResolver resourceResolver) { +public final class CompilationConfiguration { + + private final RumbleRuntimeConfiguration runtimeConfiguration; + private final ResourceResolver resourceResolver; + private final Map libraryModulesByLocation; - public CompilationConfiguration { - Objects.requireNonNull(runtimeConfiguration, "runtimeConfiguration must not be null"); - Objects.requireNonNull(resourceResolver, "resourceResolver must not be null"); + public CompilationConfiguration( + RumbleRuntimeConfiguration runtimeConfiguration, + ResourceResolver resourceResolver + ) { + this.runtimeConfiguration = Objects.requireNonNull( + runtimeConfiguration, + "runtimeConfiguration must not be null" + ); + this.resourceResolver = Objects.requireNonNull(resourceResolver, "resourceResolver must not be null"); + this.libraryModulesByLocation = new HashMap<>(); } public CompilationConfiguration(RumbleRuntimeConfiguration runtimeConfiguration) { this(runtimeConfiguration, new ResourceResolver()); } + + public RumbleRuntimeConfiguration runtimeConfiguration() { + return this.runtimeConfiguration; + } + + public ResourceResolver resourceResolver() { + return this.resourceResolver; + } + + public LibraryModule getLibraryModule(URI location) { + return this.libraryModulesByLocation.get(location); + } + + public LibraryModule cacheLibraryModule(URI location, LibraryModule module) { + LibraryModule existing = this.libraryModulesByLocation.get(location); + if (existing != null) { + return existing; + } + this.libraryModulesByLocation.put(location, module); + return module; + } + + public void removeLibraryModule(URI location, LibraryModule module) { + if (this.libraryModulesByLocation.get(location) == module) { + this.libraryModulesByLocation.remove(location); + } + } } diff --git a/src/main/java/org/rumbledb/context/RuntimeStaticContext.java b/src/main/java/org/rumbledb/context/RuntimeStaticContext.java index 37d77ff943..9a29be493a 100644 --- a/src/main/java/org/rumbledb/context/RuntimeStaticContext.java +++ b/src/main/java/org/rumbledb/context/RuntimeStaticContext.java @@ -24,6 +24,7 @@ public class RuntimeStaticContext implements Serializable { private static final long serialVersionUID = 1L; private final URI staticURI; + private final String staticURIString; /** * Query language associated with this context, which is used for error reporting and to determine the @@ -134,6 +135,7 @@ public static class RuntimeStaticContextBuilder { public static RuntimeStaticContextBuilder fromStaticContext(@NonNull StaticContext staticContext) { return builder() .staticURI(staticContext.getStaticBaseURI()) + .staticURIString(staticContext.getStaticBaseUriString()) .queryLanguage(staticContext.getQueryLanguage()) .staticallyKnownNamespaces(staticContext.getInScopeNamespaceBindings()) .staticallyKnownCollations(staticContext.getStaticallyKnownCollations()) diff --git a/src/main/java/org/rumbledb/context/StaticContext.java b/src/main/java/org/rumbledb/context/StaticContext.java index 86666c30ca..4f17d346ff 100644 --- a/src/main/java/org/rumbledb/context/StaticContext.java +++ b/src/main/java/org/rumbledb/context/StaticContext.java @@ -50,6 +50,7 @@ public class StaticContext { private String queryLanguage; private StaticContext parent; private URI staticBaseURI; + private String staticBaseUriString; private boolean emptySequenceOrderLeast; private boolean boundarySpacePreserve; private boolean copyNamespacesPreserve; @@ -96,6 +97,7 @@ public class StaticContext { public StaticContext() { this.parent = null; this.staticBaseURI = null; + this.staticBaseUriString = null; this.queryLanguage = null; this.inScopeVariables = null; this.userDefinedFunctionExecutionModes = null; @@ -118,6 +120,7 @@ public StaticContext() { public StaticContext(URI staticBaseURI, RumbleRuntimeConfiguration configuration) { this.parent = null; this.staticBaseURI = staticBaseURI; + this.staticBaseUriString = staticBaseURI == null ? null : staticBaseURI.toString(); this.queryLanguage = configuration.getQueryLanguage() != null ? configuration.getQueryLanguage() : this.queryLanguage; @@ -212,8 +215,24 @@ public URI getStaticBaseURI() { throw new OurBadException("Static base URI not set."); } + public String getStaticBaseUriString() { + if (this.staticBaseUriString != null) { + return this.staticBaseUriString; + } + if (this.parent != null) { + return this.parent.getStaticBaseUriString(); + } + throw new OurBadException("Static base URI not set."); + } + public void setStaticBaseUri(URI staticBaseURI) { this.staticBaseURI = staticBaseURI; + this.staticBaseUriString = staticBaseURI == null ? null : staticBaseURI.toString(); + } + + public void setStaticBaseUri(URI staticBaseURI, String staticBaseUriString) { + this.staticBaseURI = staticBaseURI; + this.staticBaseUriString = staticBaseUriString; } public boolean isInScope(Name varName) { diff --git a/src/main/java/org/rumbledb/errorcodes/ErrorCode.java b/src/main/java/org/rumbledb/errorcodes/ErrorCode.java index 3afaee7d83..007b022975 100644 --- a/src/main/java/org/rumbledb/errorcodes/ErrorCode.java +++ b/src/main/java/org/rumbledb/errorcodes/ErrorCode.java @@ -180,6 +180,7 @@ public String toString() { public static final ErrorCode InvalidJsoniqVersionErrorCode = registerBuiltIn("XQST0031"); public static final ErrorCode MultipleBaseURIExceptionCode = registerBuiltIn("XQST0032"); public static final ErrorCode NamespacePrefixBoundTwiceCode = registerBuiltIn("XQST0033"); + public static final ErrorCode ModuleDependencyCycleErrorCode = registerBuiltIn("XQST0093"); public static final ErrorCode DuplicateFunctionIdentifier = registerBuiltIn("XQST0034"); public static final ErrorCode DefaultCollationExceptionCode = registerBuiltIn("XQST0038"); public static final ErrorCode DuplicateParamName = registerBuiltIn("XQST0039"); diff --git a/src/main/java/org/rumbledb/exceptions/ModuleDependencyCycleException.java b/src/main/java/org/rumbledb/exceptions/ModuleDependencyCycleException.java new file mode 100644 index 0000000000..71307528a6 --- /dev/null +++ b/src/main/java/org/rumbledb/exceptions/ModuleDependencyCycleException.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.rumbledb.exceptions; + +import org.rumbledb.errorcodes.ErrorCode; + +import java.io.Serial; + +public class ModuleDependencyCycleException extends RumbleException { + + @Serial + private static final long serialVersionUID = 1L; + + public ModuleDependencyCycleException(String message, ExceptionMetadata metadata) { + super(message, ErrorCode.ModuleDependencyCycleErrorCode, metadata); + } +} diff --git a/src/main/java/org/rumbledb/expressions/AbstractNodeVisitor.java b/src/main/java/org/rumbledb/expressions/AbstractNodeVisitor.java index 2e6d022e1a..760fee46b1 100644 --- a/src/main/java/org/rumbledb/expressions/AbstractNodeVisitor.java +++ b/src/main/java/org/rumbledb/expressions/AbstractNodeVisitor.java @@ -20,6 +20,10 @@ package org.rumbledb.expressions; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Set; + import org.rumbledb.expressions.arithmetic.AdditiveExpression; import org.rumbledb.expressions.arithmetic.MultiplicativeExpression; import org.rumbledb.expressions.arithmetic.UnaryExpression; @@ -126,6 +130,8 @@ public abstract class AbstractNodeVisitor { + private final Set activeLibraryModules = Collections.newSetFromMap(new IdentityHashMap<>()); + public T visit(Node node, T argument) { return node.accept(this, argument); } @@ -152,7 +158,14 @@ public T visitMainModule(MainModule expression, T argument) { } public T visitLibraryModule(LibraryModule expression, T argument) { - return defaultAction(expression, argument); + if (!this.activeLibraryModules.add(expression)) { + return argument; + } + try { + return defaultAction(expression, argument); + } finally { + this.activeLibraryModules.remove(expression); + } } public T visitProlog(Prolog expression, T argument) { diff --git a/src/main/java/org/rumbledb/expressions/module/LibraryModule.java b/src/main/java/org/rumbledb/expressions/module/LibraryModule.java index fb25ac8d6a..4eb316c182 100644 --- a/src/main/java/org/rumbledb/expressions/module/LibraryModule.java +++ b/src/main/java/org/rumbledb/expressions/module/LibraryModule.java @@ -50,6 +50,10 @@ public String getNamespace() { return this.namespace; } + public void setNamespace(String namespace) { + this.namespace = namespace; + } + public void setStaticContext(StaticContext staticContext) { this.staticContext = staticContext; } @@ -92,4 +96,3 @@ public void serializeToJSONiq(StringBuilder sb, int indent) { this.prolog.serializeToJSONiq(sb, indent); } } - diff --git a/src/main/java/org/rumbledb/items/AnyURIItem.java b/src/main/java/org/rumbledb/items/AnyURIItem.java index 6eaf6a2c50..0ede049020 100644 --- a/src/main/java/org/rumbledb/items/AnyURIItem.java +++ b/src/main/java/org/rumbledb/items/AnyURIItem.java @@ -37,6 +37,7 @@ public class AnyURIItem implements Item { @Serial private static final long serialVersionUID = 1L; + private String lexicalValue; private URI value; public AnyURIItem() { @@ -45,12 +46,16 @@ public AnyURIItem() { public AnyURIItem(String value) { super(); + if (value == null) { + throw new IllegalArgumentException(); + } + this.lexicalValue = value; this.value = parseAnyURIString(value); } @Override public Item copy(boolean mutable) { - return new AnyURIItem(this.value.toString()); + return new AnyURIItem(this.lexicalValue); } @Override @@ -73,23 +78,23 @@ static URI parseAnyURIString(String anyURIString) throws IllegalArgumentExceptio try { return new URI(anyURIString); } catch (URISyntaxException e) { - throw new IllegalArgumentException(e.getMessage()); + return null; } } @Override public String getStringValue() { - return this.value.toString(); + return this.lexicalValue; } @Override public boolean getEffectiveBooleanValue() { - return !this.value.toString().isEmpty(); + return !this.lexicalValue.isEmpty(); } @Override public int hashCode() { - return this.value.hashCode(); + return this.lexicalValue.hashCode(); } public URI getValue() { diff --git a/src/main/java/org/rumbledb/runtime/functions/strings/StaticBaseURIFunctionIterator.java b/src/main/java/org/rumbledb/runtime/functions/strings/StaticBaseURIFunctionIterator.java index a92389e2d8..00e33c13a8 100644 --- a/src/main/java/org/rumbledb/runtime/functions/strings/StaticBaseURIFunctionIterator.java +++ b/src/main/java/org/rumbledb/runtime/functions/strings/StaticBaseURIFunctionIterator.java @@ -25,6 +25,6 @@ public StaticBaseURIFunctionIterator( @Override public Item materializeFirstItemOrNull(DynamicContext context) { - return ItemFactory.getInstance().createAnyURIItem(this.staticContext.getStaticURI().toString()); + return ItemFactory.getInstance().createAnyURIItem(this.staticContext.getStaticURIString()); } } diff --git a/src/test/resources/test_files/runtime/LibraryModules/ModuleImport6.jq b/src/test/resources/test_files/runtime/LibraryModules/ModuleImport6.jq index d445884062..e2c2014f0e 100644 --- a/src/test/resources/test_files/runtime/LibraryModules/ModuleImport6.jq +++ b/src/test/resources/test_files/runtime/LibraryModules/ModuleImport6.jq @@ -1,4 +1,4 @@ (:JIQS: ShouldRun; Output="20" :) -import module namespace my-module = "modulerepeatedimport.jq"; +import module namespace my-module = "http://example.com/modulerepeatedimport.jq" +at "modulerepeatedimport.jq"; my-module:func() - diff --git a/src/test/resources/test_files/runtime/LibraryModules/ModuleImport7.jq b/src/test/resources/test_files/runtime/LibraryModules/ModuleImport7.jq index 1a4ab73dd2..e2c2014f0e 100644 --- a/src/test/resources/test_files/runtime/LibraryModules/ModuleImport7.jq +++ b/src/test/resources/test_files/runtime/LibraryModules/ModuleImport7.jq @@ -1,4 +1,4 @@ (:JIQS: ShouldRun; Output="20" :) -import module namespace my-module = "https://raw.githubusercontent.com/RumbleDB/rumble/master/src/test/resources/test_files/runtime/LibraryModules/modulerepeatedimport.jq"; +import module namespace my-module = "http://example.com/modulerepeatedimport.jq" +at "modulerepeatedimport.jq"; my-module:func() - diff --git a/src/test/resources/test_files/runtime/LibraryModules/modulerepeatedimport.jq b/src/test/resources/test_files/runtime/LibraryModules/modulerepeatedimport.jq index 3e4b842a89..aa203b4615 100644 --- a/src/test/resources/test_files/runtime/LibraryModules/modulerepeatedimport.jq +++ b/src/test/resources/test_files/runtime/LibraryModules/modulerepeatedimport.jq @@ -1,5 +1,5 @@ (:JIQS: ShouldNotParse; ErrorCode="XPST0003"; ErrorMetadata="LINE:2:COLUMN:0:" :) -module namespace my-module = "modulerepeatedimport.jq"; +module namespace my-module = "http://example.com/modulerepeatedimport.jq"; import module namespace mod1 = "moduleimportingmodule.jq"; import module namespace mod2 = "module.jq";