Skip to content

Commit

Permalink
Allow creating a node without an explicit ID.
Browse files Browse the repository at this point in the history
This commit makes it possible to use a "create <nodetype>" command
without specifying an explicit ID, just a label:

  create class "my new class"

This is equivalent to:

  create class AUTOID:<random_value> "my new class"

This change is accompanied by several refactoring changes:

* The IEntityLabelResolver interface (which gains two more methods to
  support the feature above) is renamed to ILabelResolver.
* The ParseTree2ChangeVisitor class is made less flexible, as it is only
  ever intended to be used internally by the KGCLReader class, not by
  client code.
  • Loading branch information
gouttegd committed Sep 1, 2024
1 parent 74c913e commit eb8e350
Show file tree
Hide file tree
Showing 12 changed files with 183 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ removeDefinition: 'remove' 'definition' 'for' id;

changeDefinition: 'change' 'definition' 'of' id ('from' old_definition=text)? 'to' new_definition=text;

newNode : 'create' nodeType id label=text;
newNode : 'create' nodeType id? label=text;

newEdge : 'create' 'edge' subject_id=id predicate_id=id object_id=id;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
/**
* An object that can resolve labels into proper entity identifiers.
*/
public interface IEntityLabelResolver {
public interface ILabelResolver {

/**
* Finds the identifier corresponding to the given label.
Expand All @@ -32,4 +32,27 @@ public interface IEntityLabelResolver {
*/
public String resolve(String label);

/**
* Registers a new label-to-identifier mapping.
*
* @param label The label to register.
* @param identifier Its corresponding identifier.
*/
public void add(String label, String identifier);

/**
* Mints a new identifier for the given label.
* <p>
* This method is used when a KGCL “create” instruction does not include an
* identifier for the node to be created (e.g.,
* {@code create class 'my new class'}). It shall return a new identifier for
* the node to be created.
* <p>
* Any subsequent call to {@link #resolve(String)} with the same label shall
* return the same identifier.
*
* @param label The label for which an identifier is requested.
* @return The newly minted identifier.
*/
public String getNewId(String label);
}
12 changes: 6 additions & 6 deletions core/src/main/java/org/incenp/obofoundry/kgcl/KGCLHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public static List<Change> parse(String kgcl, PrefixManager prefixManager, List<
* @return A KGCL changeset.
*/
public static List<Change> parse(String kgcl, PrefixManager prefixManager, List<KGCLSyntaxError> errors,
IEntityLabelResolver labelResolver) {
ILabelResolver labelResolver) {
return doParse(kgcl, prefixManager, errors, labelResolver);
}

Expand All @@ -98,7 +98,7 @@ public static List<Change> parse(String kgcl, PrefixManager prefixManager, List<
* @return A KGCL changeset.
*/
public static List<Change> parse(String kgcl, Map<String, String> prefixMap, List<KGCLSyntaxError> errors,
IEntityLabelResolver labelResolver) {
ILabelResolver labelResolver) {
PrefixManager prefixManager = null;
if ( prefixMap != null && !prefixMap.isEmpty() ) {
prefixManager = new DefaultPrefixManager();
Expand All @@ -108,7 +108,7 @@ public static List<Change> parse(String kgcl, Map<String, String> prefixMap, Lis
}

private static List<Change> doParse(String kgcl, PrefixManager prefixManager, List<KGCLSyntaxError> errors,
IEntityLabelResolver labelResolver) {
ILabelResolver labelResolver) {
List<Change> changeset = null;

if ( kgcl != null ) {
Expand Down Expand Up @@ -168,7 +168,7 @@ public static List<Change> parse(File kgcl, PrefixManager prefixManager, List<KG
* @throws IOException If any non-KGCL I/O error occurs.
*/
public static List<Change> parse(File kgcl, PrefixManager prefixManager, List<KGCLSyntaxError> errors,
IEntityLabelResolver labelResolver) throws IOException {
ILabelResolver labelResolver) throws IOException {
return doParse(new KGCLReader(kgcl), prefixManager, errors, labelResolver);
}

Expand All @@ -187,7 +187,7 @@ public static List<Change> parse(File kgcl, PrefixManager prefixManager, List<KG
* @throws IOException If any non-KGCL I/O error occurs.
*/
public static List<Change> parse(File kgcl, Map<String, String> prefixMap, List<KGCLSyntaxError> errors,
IEntityLabelResolver labelResolver) throws IOException {
ILabelResolver labelResolver) throws IOException {
PrefixManager prefixManager = null;
if ( prefixMap != null && !prefixMap.isEmpty() ) {
prefixManager = new DefaultPrefixManager();
Expand All @@ -197,7 +197,7 @@ public static List<Change> parse(File kgcl, Map<String, String> prefixMap, List<
}

private static List<Change> doParse(KGCLReader reader, PrefixManager prefixManager, List<KGCLSyntaxError> errors,
IEntityLabelResolver labelResolver) {
ILabelResolver labelResolver) {
reader.setPrefixManager(prefixManager);
reader.setLabelResolver(labelResolver);

Expand Down
25 changes: 18 additions & 7 deletions core/src/main/java/org/incenp/obofoundry/kgcl/KGCLReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
public class KGCLReader {
private KGCLLexer lexer;
private PrefixManager prefixManager;
private IEntityLabelResolver labelResolver;
private ILabelResolver labelResolver;
private ErrorListener errorListener = new ErrorListener();
private List<Change> changeSet = new ArrayList<Change>();
private boolean hasRead = false;
Expand Down Expand Up @@ -201,13 +201,18 @@ public void setPrefixMap(Map<String, String> map) {
* entity by its label rather than by its identifier. It is the resolver’s
* responsibility to find the entity referenced by the label.
* <p>
* If not set or {@code null} (which is the case by default), the reader will be
* unable to resolve labels and any use of a label in place of an identifier
* will lead to an error.
* If not set or {@code null} (which is the case by default), the reader will
* only be able to resolve labels (resulting in a parsing error) unless they
* have been used in a prior {@code create...} instruction, as in:
*
* <pre>
* create class EX:0001 "my label"
* create exact synonym "my synonym" for "my label"
* </pre>
*
* @param resolver The resolver to use.
*/
public void setLabelResolver(IEntityLabelResolver resolver) {
public void setLabelResolver(ILabelResolver resolver) {
labelResolver = resolver;
}

Expand Down Expand Up @@ -291,8 +296,7 @@ private boolean doParse(KGCLLexer lexer, boolean reset) {

ParseTree tree = parser.changeset();
if ( errorListener.errors.size() == nErrors ) {
ParseTree2ChangeVisitor visitor = new ParseTree2ChangeVisitor(prefixManager, changeSet);
visitor.setLabelResolver(labelResolver);
ParseTree2ChangeVisitor visitor = new ParseTree2ChangeVisitor(prefixManager, getLabelResolver(), changeSet);
visitor.addErrorListener(errorListener);
visitor.visit(tree);
}
Expand Down Expand Up @@ -348,6 +352,13 @@ public List<KGCLSyntaxError> getErrors() {
return errorListener.errors;
}

private ILabelResolver getLabelResolver() {
if ( labelResolver == null ) {
labelResolver = new SimpleLabelResolver();
}
return labelResolver;
}

private class ErrorListener extends BaseErrorListener implements IParseTreeErrorListener {

private ArrayList<KGCLSyntaxError> errors = new ArrayList<KGCLSyntaxError>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* KGCL-Java - KGCL library for Java
* Copyright © 2024 Damien Goutte-Gattat
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the Gnu General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package org.incenp.obofoundry.kgcl;

import java.util.HashMap;
import java.util.UUID;

/**
* A basic implementation of the {@link ILabelResolver} interface.
* <p>
* This implementation is backed up by a simple dictionary mapping labels to
* their corresponding identifiers. When a new ID is requested (with
* {@link #getNewId(String)}), it mints a temporary ID suitable for use with the
* {@link org.incenp.obofoundry.kgcl.AutoIDAllocator} class.
*/
public class SimpleLabelResolver implements ILabelResolver {

private HashMap<String, String> idMap = new HashMap<String, String>();

@Override
public String resolve(String label) {
return idMap.get(label);
}

@Override
public String getNewId(String label) {
String newId = AutoIDAllocator.AUTOID_BASE_IRI + UUID.randomUUID().toString();
idMap.put(label, newId);
return newId;
}

@Override
public void add(String label, String identifier) {
idMap.put(label, identifier);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import java.util.HashMap;
import java.util.HashSet;

import org.incenp.obofoundry.kgcl.IEntityLabelResolver;
import org.incenp.obofoundry.kgcl.SimpleLabelResolver;
import org.semanticweb.owlapi.model.OWLAnnotationAssertionAxiom;
import org.semanticweb.owlapi.model.OWLEntity;
import org.semanticweb.owlapi.model.OWLOntology;
Expand All @@ -31,7 +31,8 @@
* An object to resolve labels into identifiers using the {@code rdfs:label}
* annotations of an ontology’s entities.
*/
public class OntologyBasedLabelResolver implements IEntityLabelResolver {
public class OntologyBasedLabelResolver extends SimpleLabelResolver
{

private HashMap<String, String> idMap = new HashMap<String, String>();
private OWLOntology ontology;
Expand All @@ -48,10 +49,20 @@ public OntologyBasedLabelResolver(OWLOntology ontology) {

@Override
public String resolve(String label) {
if ( idMap.isEmpty() ) {
buildIdMap();
// We first lookup in the parent's dictionary, in case the label has a newly
// minted ID. Such IDs takes precedence over the ontology's contents.
String resolved = super.resolve(label);
if ( resolved == null ) {
if ( idMap.isEmpty() ) {
// Rather than querying the ontology for each lookup, we build a one-time map of
// all labels. Since this requires iterating over the entire ontology, we do
// that lazily, so that we may in fact not have to do it at all if we are never
// asked to resolve an identifier.
buildIdMap();
}
resolved = idMap.get(label);
}
return idMap.get(label);
return resolved;
}

private void buildIdMap() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import java.util.List;

import org.antlr.v4.runtime.Token;
import org.incenp.obofoundry.kgcl.IEntityLabelResolver;
import org.incenp.obofoundry.kgcl.ILabelResolver;
import org.incenp.obofoundry.kgcl.KGCLReader;
import org.incenp.obofoundry.kgcl.model.AddNodeToSubset;
import org.incenp.obofoundry.kgcl.model.Change;
Expand Down Expand Up @@ -54,8 +54,6 @@
import org.incenp.obofoundry.kgcl.model.RemoveTextDefinition;
import org.incenp.obofoundry.kgcl.model.SynonymReplacement;
import org.incenp.obofoundry.kgcl.model.TextDefinitionReplacement;
import org.incenp.obofoundry.kgcl.parser.KGCLBaseVisitor;
import org.incenp.obofoundry.kgcl.parser.KGCLParser;
import org.incenp.obofoundry.kgcl.parser.KGCLParser.IdContext;
import org.incenp.obofoundry.kgcl.parser.KGCLParser.TextContext;
import org.semanticweb.owlapi.model.PrefixManager;
Expand All @@ -70,38 +68,26 @@
public class ParseTree2ChangeVisitor extends KGCLBaseVisitor<Void> {

private PrefixManager prefixManager;
private IEntityLabelResolver labelResolver;
private ILabelResolver labelResolver;
private List<Change> changes;
private List<IParseTreeErrorListener> errorListeners = new ArrayList<IParseTreeErrorListener>();
private String currentId;
private boolean isBogus = false;

/**
* Creates a new visitor with the specified prefix manager.
*
* @param prefixManager An OWL API prefix manager that will be used to convert
* short identifiers (“CURIEs”) into their corresponding
* full-length, canonical forms. May be {@code null}, in
* which case short identifiers will all be assumed to be
* OBO-style CURIEs in the
* {@code http://purl.obolibrary.org/obo/} namespace.
*/
public ParseTree2ChangeVisitor(PrefixManager prefixManager) {
this.prefixManager = prefixManager;
changes = new ArrayList<Change>();
}

/**
* Creates a new visitor with the specified prefix manager and list to store the
* changes.
*
* @param prefixManager An OWL API prefix manager to convert short identifiers
* into their full-length forms. May be {@code null}.
* @param labelResolver The resolver to use to resolve labels into identifiers.
* @param changes The list where changes built from the parse tree will be
* accumulated.
*/
public ParseTree2ChangeVisitor(PrefixManager prefixManager, List<Change> changes) {
public ParseTree2ChangeVisitor(PrefixManager prefixManager, ILabelResolver labelResolver,
List<Change> changes) {
this.prefixManager = prefixManager;
this.labelResolver = labelResolver;
this.changes = changes;
}

Expand All @@ -114,26 +100,6 @@ public void addErrorListener(IParseTreeErrorListener listener) {
errorListeners.add(listener);
}

/**
* Sets the label resolver, to be used when an entity is referenced by its label
* rather than by its identifier.
*
* @param resolver The resolver to use.
*/
public void setLabelResolver(IEntityLabelResolver resolver) {
labelResolver = resolver;
}

/**
* Gets the changes obtained from converting the parse tree. Call this function
* after having visited the entire parse tree to get the entire set of changes.
*
* @return The list of change objects.
*/
public List<Change> getChangeSet() {
return changes;
}

@Override
public Void visitRename(KGCLParser.RenameContext ctx) {
NodeRename change = new NodeRename();
Expand Down Expand Up @@ -298,7 +264,16 @@ public Void visitNewNode(KGCLParser.NewNodeContext ctx) {
break;
}

change.setAboutNode(getNode(ctx.id()));
if ( ctx.id() != null ) {
change.setAboutNode(getNode(ctx.id()));
labelResolver.add(unquote(ctx.label.string().getText()), currentId);
} else {
String newId = labelResolver.getNewId(unquote(ctx.label.string().getText()));
Node aboutNode = new Node();
aboutNode.setId(newId);
change.setAboutNode(aboutNode);
}

change.getAboutNode().setOwlType(type);
setNewValue(ctx.label, change);

Expand Down Expand Up @@ -437,13 +412,13 @@ public Void visitIdAsCURIE(KGCLParser.IdAsCURIEContext ctx) {

@Override
public Void visitIdAsLabel(KGCLParser.IdAsLabelContext ctx) {
if ( labelResolver != null ) {
currentId = labelResolver.resolve(unquote(ctx.string().getText()));
if ( currentId != null ) {
return null;
}
String label = unquote(ctx.string().getText());

currentId = labelResolver.resolve(label);
if ( currentId == null ) {
onParseTreeError(ctx.getStart(), String.format("Unresolved label %s", label));
}
onParseTreeError(ctx.getStart(), String.format("Unresolved label %s", ctx.string().getText()));

return null;
}

Expand Down
4 changes: 2 additions & 2 deletions core/src/site/apt/library.apt
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@ reader.read();
The KGCL-DSL syntax also allows referencing nodes using their labels
rather than their identifiers. For that to be possible, the
<<<KGCLReader>>> object must be provided with an implementation of the
{{{./apidocs/org/incenp/obofoundry/kgcl/IEntityLabelResolver.html}IEntityLabelResolver}}
interface so that the labels can be resolved into proper identifiers.
{{{./apidocs/org/incenp/obofoundry/kgcl/ILabelResolver.html}LabelResolver}}
object so that the labels can be resolved into proper identifiers.
The library provides an implementation that can resolve labels based
on a OWL ontology:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ void testStringParsingWithErrorCollection() {
void testUsingLabelsAsIds() {
List<Change> changeset = null;
List<KGCLSyntaxError> errors = new ArrayList<KGCLSyntaxError>();
IEntityLabelResolver resolver = new OntologyBasedLabelResolver(ontology);
SimpleLabelResolver resolver = new OntologyBasedLabelResolver(ontology);

changeset = KGCLHelper.parse("obsolete 'LaReine'", prefixManager, errors, resolver);
Assertions.assertEquals(1, changeset.size());
Expand Down
Loading

0 comments on commit eb8e350

Please sign in to comment.