Skip to content

Commit b44606a

Browse files
committed
Allow overriding the default logic for resolving imports.
So far, the logic to resolve import declarations within a LinkML schema has been embedded within the SchemaDocument class, and was not modifiable. This commit introduces a ISchemaResolver interface for an object that can resolve an import name into a usable schema location (ISchemaSource). The pre-existing logic is moved to a default implementation of that interface, and SchemaDocument is given an additional constructor that can accept a ISchemaResolver provided by the client code -- thereby giving client code the ability to customise the logic for resolving imports as needed. In addition, the default resolver now checks whether a schema name that is supposed to point to a local file, actually points to an _existing_ file, and errors out if it does not.
1 parent 396bf29 commit b44606a

3 files changed

Lines changed: 177 additions & 32 deletions

File tree

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* LinkML-Java - LinkML library for Java
3+
* Copyright © 2026 Damien Goutte-Gattat
4+
*
5+
* Redistribution and use in source and binary forms, with or without
6+
* modification, are permitted provided that the following conditions
7+
* are met:
8+
*
9+
* (1) Redistributions of source code must retain the above copyright
10+
* notice, this list of conditions and the following disclaimer.
11+
*
12+
* (2) Redistributions in binary form must reproduce the above
13+
* copyright notice, this list of conditions and the following
14+
* disclaimer in the documentation and/or other materials provided
15+
* with the distribution.
16+
*
17+
* (3) Neither the name of the copyright holder nor the names its
18+
* contributors may be used to endorse or promote products derived
19+
* from this software without specific prior written permission.
20+
*
21+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS
22+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
24+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
25+
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26+
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27+
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
28+
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
29+
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30+
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
31+
* WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32+
* POSSIBILITY OF SUCH DAMAGE.
33+
*/
34+
35+
package org.incenp.linkml.schema;
36+
37+
import java.io.File;
38+
import java.net.MalformedURLException;
39+
40+
/**
41+
* The schema resolver that implements the default behaviour for resolving
42+
* <code>imports</code> declarations within a LinkML schema.
43+
* <p>
44+
* If the name to resolve contains a colon, this resolver assumes it is a
45+
* relative file name, and attempts to find it on the local file system.
46+
* Otherwise, it assumes the name is a URI pointing to a remote resource.
47+
* <p>
48+
* Of note, this resolver automatically and silently redirects the
49+
* <code>https://w3id.org/linkml/types.yaml</code> schema name to a version that
50+
* is embedded with the LinkML-Java runtime. That schema is expected to be
51+
* imported in virtually all LinkML schemas, so we don’t want to have to always
52+
* fetch it from a remote server.
53+
*/
54+
public class DefaultSchemaResolver implements ISchemaResolver {
55+
56+
private final static String TYPES_SCHEMA = "https://w3id.org/linkml/types.yaml";
57+
private final static String UNRESOLVABLE_SCHEMA = "Cannot resolve schema name '%s'";
58+
59+
@Override
60+
public ISchemaSource resolve(String name, String base) throws InvalidSchemaException {
61+
if (!name.contains(":")) {
62+
// Local file, relative to the base directory
63+
File file = new File(base, name);
64+
if ( !file.exists() ) {
65+
throw new InvalidSchemaException(String.format(UNRESOLVABLE_SCHEMA, name));
66+
}
67+
return new FileSchemaSource(file);
68+
}
69+
70+
if ( name.equals(TYPES_SCHEMA) ) {
71+
// Redirect the standard linkml:types schema to the embedded version.
72+
return new EmbeddedSchemaSource("types.yaml");
73+
}
74+
try {
75+
return new URLSchemaSource(name);
76+
} catch ( MalformedURLException e ) {
77+
throw new InvalidSchemaException(String.format(UNRESOLVABLE_SCHEMA, name), e);
78+
}
79+
}
80+
81+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* LinkML-Java - LinkML library for Java
3+
* Copyright © 2026 Damien Goutte-Gattat
4+
*
5+
* Redistribution and use in source and binary forms, with or without
6+
* modification, are permitted provided that the following conditions
7+
* are met:
8+
*
9+
* (1) Redistributions of source code must retain the above copyright
10+
* notice, this list of conditions and the following disclaimer.
11+
*
12+
* (2) Redistributions in binary form must reproduce the above
13+
* copyright notice, this list of conditions and the following
14+
* disclaimer in the documentation and/or other materials provided
15+
* with the distribution.
16+
*
17+
* (3) Neither the name of the copyright holder nor the names its
18+
* contributors may be used to endorse or promote products derived
19+
* from this software without specific prior written permission.
20+
*
21+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS
22+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
24+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
25+
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26+
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27+
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
28+
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
29+
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30+
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
31+
* WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32+
* POSSIBILITY OF SUCH DAMAGE.
33+
*/
34+
35+
package org.incenp.linkml.schema;
36+
37+
/**
38+
* An interface to resolve the name of a schema into a {@link ISchemaSource}.
39+
* <p>
40+
* That interface is primarily intended to allow client code to customise the
41+
* way that import declarations within a LinkML schema are resolved.
42+
*/
43+
public interface ISchemaResolver {
44+
45+
/**
46+
* Resolves the given schema name into a schema source object.
47+
*
48+
* @param name The name of the schema to resolve. Of note, if the name was
49+
* originally provided as a Curie, this method will receive the
50+
* <em>expanded</em> form of the original name.
51+
* @param base The base location from which to resolve relative names.
52+
* @return A {@link ISchemaSource} object that can be used to access the content
53+
* of the schema.
54+
* @throws InvalidSchemaException If the name cannot be resolved into a schema
55+
* source.
56+
*/
57+
public ISchemaSource resolve(String name, String base) throws InvalidSchemaException;
58+
}

core/src/main/java/org/incenp/linkml/schema/SchemaDocument.java

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636

3737
import java.io.File;
3838
import java.io.IOException;
39-
import java.net.MalformedURLException;
4039
import java.util.ArrayList;
4140
import java.util.Collection;
4241
import java.util.HashMap;
@@ -63,10 +62,8 @@
6362
*/
6463
public class SchemaDocument {
6564

66-
private final static String TYPES_SCHEMA = "https://w3id.org/linkml/types.yaml";
6765
private final static String INVALID_YAML = "Cannot read YAML document";
6866
private final static String INVALID_LINKML = "Cannot parse schema from YAML document";
69-
private final static String UNRESOLVABLE_IMPORT = "Cannot resolve import name '%s'";
7067

7168
private SchemaDefinition rootSchema;
7269
private List<SchemaDefinition> importedSchemas = new ArrayList<>();
@@ -78,6 +75,8 @@ public class SchemaDocument {
7875
private Map<String, Map<String, SlotDefinition>> allAttributes = new HashMap<>();
7976
private Map<String, Map<String, SlotDefinition>> allSlotUsages = new HashMap<>();
8077

78+
private ISchemaResolver importResolver;
79+
8180
/**
8281
* Creates a new instance from the specified file.
8382
*
@@ -86,6 +85,39 @@ public class SchemaDocument {
8685
* @throws InvalidSchemaException If the file is not a valid schema.
8786
*/
8887
public SchemaDocument(File source) throws IOException, InvalidSchemaException {
88+
importResolver = new DefaultSchemaResolver();
89+
rootSchema = parseSchema(new FileSchemaSource(source));
90+
}
91+
92+
/**
93+
* Creates a new instance from the specified file, using a custom import
94+
* resolver.
95+
*
96+
* @param source The source file from which to parse the LinkML schema.
97+
* @param importResolver The resolver to use to resolve import names into schema
98+
* source objects.
99+
* @throws IOException If we cannot read the specified file.
100+
* @throws InvalidSchemaException If the file is not a valid schema.
101+
*/
102+
public SchemaDocument(File source, ISchemaResolver importResolver) throws IOException, InvalidSchemaException {
103+
this.importResolver = importResolver;
104+
rootSchema = parseSchema(new FileSchemaSource(source));
105+
}
106+
107+
/**
108+
* Creates a new instance from the specified source, using a custom import
109+
* resolver.
110+
*
111+
* @param source The source location from which to obtain the LinkML
112+
* schema.
113+
* @param importResolver The resolver to use to resolve import names into schema
114+
* source objects.
115+
* @throws IOException If we cannot read the specified file.
116+
* @throws InvalidSchemaException If the file is not a valid schema.
117+
*/
118+
public SchemaDocument(ISchemaSource source, ISchemaResolver importResolver)
119+
throws IOException, InvalidSchemaException {
120+
this.importResolver = importResolver;
89121
rootSchema = parseSchema(source);
90122
}
91123

@@ -225,13 +257,13 @@ public SlotDefinition getSlotUsage(String className, String slotName) {
225257
}
226258

227259
// The entry point for the actual parsing code
228-
private SchemaDefinition parseSchema(File file)
260+
private SchemaDefinition parseSchema(ISchemaSource source)
229261
throws IOException, InvalidSchemaException {
230262
ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
231263
ConverterContext ctx = new ConverterContext();
232264

233265
// Parse the top-level schema and all its imports recursively
234-
SchemaDefinition schema = parseSchema(new FileSchemaSource(file), mapper, ctx);
266+
SchemaDefinition schema = parseSchema(source, mapper, ctx);
235267

236268
try {
237269
// All schemas have been read, so we can resolve all references
@@ -264,7 +296,7 @@ private SchemaDefinition parseSchema(ISchemaSource source, ObjectMapper mapper,
264296
importedSources.add(source);
265297
if ( schema.getImports() != null ) {
266298
for ( String importName : schema.getImports() ) {
267-
ISchemaSource importSource = resolveImport(importName, schema, source.getBase());
299+
ISchemaSource importSource = importResolver.resolve(importName + ".yaml", source.getBase());
268300
if ( !importedSources.contains(importSource) ) {
269301
SchemaDefinition importedSchema = parseSchema(importSource, mapper, ctx);
270302
importedSchemas.add(importedSchema);
@@ -311,30 +343,4 @@ private SchemaDefinition parseSchema(ISchemaSource source, ObjectMapper mapper,
311343

312344
return schema;
313345
}
314-
315-
private ISchemaSource resolveImport(String name, SchemaDefinition schema, String base)
316-
throws InvalidSchemaException {
317-
name += ".yaml";
318-
if ( !name.contains(":") ) {
319-
// Local file, relative to the directory containing the importing schema
320-
if ( base != null ) {
321-
return new FileSchemaSource(base + File.separator + name);
322-
} else {
323-
return new FileSchemaSource(name);
324-
}
325-
}
326-
327-
if ( name.equals(TYPES_SCHEMA) ) {
328-
// Redirect the standard linkml:types schema to the embedded version. That
329-
// schema is expected to be imported in virtually all LinkML schemas, so we
330-
// don't want to have to always fetch it from a remote server.
331-
// FIXME: We should probably provide a way to override this behaviour.
332-
return new EmbeddedSchemaSource("types.yaml");
333-
}
334-
try {
335-
return new URLSchemaSource(name);
336-
} catch ( MalformedURLException e ) {
337-
throw new InvalidSchemaException(String.format(UNRESOLVABLE_IMPORT, name), e);
338-
}
339-
}
340346
}

0 commit comments

Comments
 (0)