Skip to content

Commit 179ab81

Browse files
authored
Add odk:obsolete-replace command (#3)
Adding the odk:obsolete-replace command to replace a key command of the owltools stack, eg: ``` owltools --use-catalog edit.obo --obsolete-replace http://purl.obolibrary.org/obo/VBO_0000012 http://purl.obolibrary.org/obo/VBO_0000038 -o -f obo edit.obo ``` The command essentially: 1. obsoletes one term 2. moves selected properties over to the replacement term I changed the interface a little but (for now) left the (owltools) implementation more or less what it was. I tested this in Mondo and it works as expected.
1 parent 80ffb70 commit 179ab81

File tree

7 files changed

+360
-2
lines changed

7 files changed

+360
-2
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ The following commands are provided by the plugin:
1414
* `odk:subset`: to create ontology subsets;
1515
* `odk:check-align`: to check the alignment of an ontology against an
1616
upper-level ontology and/or arbitrary root terms;
17-
* `odk:normalize`: to “normalise” an ontology,
18-
* `odk:import`: to add/remove import declarations.
17+
* `odk:normalize`: to "normalise" an ontology;
18+
* `odk:import`: to add/remove import declarations;
19+
* `odk:obsolete-replace`: to obsolete an entity and replace all its usages
20+
with another entity.
1921

2022
Building
2123
--------

src/main/java/org/incenp/obofoundry/odk/Constants.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,16 @@ public class Constants {
3939
public static final IRI COB_IRI = IRI.create("http://purl.obolibrary.org/obo/cob.owl");
4040

4141
public static final IRI PREFERRED_ROOT_PROPERTY = IRI.create("http://purl.obolibrary.org/obo/IAO_0000700");
42+
43+
public static final IRI RDFS_LABEL = IRI.create("http://www.w3.org/2000/01/rdf-schema#label");
44+
45+
public static final IRI RDFS_COMMENT = IRI.create("http://www.w3.org/2000/01/rdf-schema#comment");
46+
47+
public static final IRI IAO_DEFINITION = IRI.create("http://purl.obolibrary.org/obo/IAO_0000115");
48+
49+
public static final IRI HAS_EXACT_SYNONYM = IRI.create(OIO_PREFIX + "hasExactSynonym");
50+
51+
public static final IRI HAS_DBXREF = IRI.create(OIO_PREFIX + "hasDbXref");
52+
53+
public static final IRI TERM_REPLACED_BY = IRI.create("http://purl.obolibrary.org/obo/IAO_0100001");
4254
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* ODK ROBOT Plugin
3+
* Copyright © 2025 Nico Matentzoglu
4+
*
5+
* This program is free software; you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
package org.incenp.obofoundry.odk;
20+
21+
import org.semanticweb.owlapi.model.OWLClass;
22+
import org.semanticweb.owlapi.model.OWLDataFactory;
23+
import org.semanticweb.owlapi.model.OWLDataProperty;
24+
import org.semanticweb.owlapi.model.OWLEntity;
25+
import org.semanticweb.owlapi.model.OWLNamedIndividual;
26+
import org.semanticweb.owlapi.model.OWLObjectProperty;
27+
import org.semanticweb.owlapi.util.OWLObjectDuplicator;
28+
29+
/**
30+
* A visitor that replaces all occurrences of one entity with another in an
31+
* axiom.
32+
* <p>
33+
* This visitor extends {@link OWLObjectDuplicator} to traverse an axiom and
34+
* replace all occurrences of a specified entity with a replacement entity.
35+
*/
36+
class EntityReplacementVisitor extends OWLObjectDuplicator {
37+
38+
private final OWLEntity obsolete;
39+
private final OWLEntity replacement;
40+
41+
/**
42+
* Creates a new entity replacement visitor.
43+
*
44+
* @param factory The data factory to use.
45+
* @param obsolete The entity to replace.
46+
* @param replacement The replacement entity.
47+
*/
48+
public EntityReplacementVisitor(OWLDataFactory factory, OWLEntity obsolete, OWLEntity replacement) {
49+
super(factory);
50+
this.obsolete = obsolete;
51+
this.replacement = replacement;
52+
}
53+
54+
@Override
55+
public void visit(OWLClass cls) {
56+
if (cls.equals(obsolete)) {
57+
setLastObject((OWLClass) replacement);
58+
} else {
59+
setLastObject(cls);
60+
}
61+
}
62+
63+
@Override
64+
public void visit(OWLObjectProperty property) {
65+
if (property.equals(obsolete)) {
66+
setLastObject((OWLObjectProperty) replacement);
67+
} else {
68+
setLastObject(property);
69+
}
70+
}
71+
72+
@Override
73+
public void visit(OWLDataProperty property) {
74+
if (property.equals(obsolete)) {
75+
setLastObject((OWLDataProperty) replacement);
76+
} else {
77+
setLastObject(property);
78+
}
79+
}
80+
81+
@Override
82+
public void visit(OWLNamedIndividual individual) {
83+
if (individual.equals(obsolete)) {
84+
setLastObject((OWLNamedIndividual) replacement);
85+
} else {
86+
setLastObject(individual);
87+
}
88+
}
89+
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/*
2+
* ODK ROBOT Plugin
3+
* Copyright © 2025 Nico Matentzoglu
4+
* This Command was strongly inspired by https://github.com/owlcollab/owltools/blob/master/OWLTools-Runner/src/main/java/owltools/cli/CommandRunner.java
5+
*
6+
* This program is free software; you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
20+
package org.incenp.obofoundry.odk;
21+
22+
import java.util.Collections;
23+
import java.util.HashSet;
24+
import java.util.Set;
25+
26+
import org.apache.commons.cli.CommandLine;
27+
import org.obolibrary.robot.CommandState;
28+
import org.semanticweb.owlapi.model.IRI;
29+
import org.semanticweb.owlapi.model.OWLAnnotation;
30+
import org.semanticweb.owlapi.model.OWLAnnotationAssertionAxiom;
31+
import org.semanticweb.owlapi.model.OWLAnnotationProperty;
32+
import org.semanticweb.owlapi.model.OWLAxiom;
33+
import org.semanticweb.owlapi.model.OWLDataFactory;
34+
import org.semanticweb.owlapi.model.OWLEntity;
35+
import org.semanticweb.owlapi.model.OWLOntology;
36+
import org.semanticweb.owlapi.model.OWLOntologyManager;
37+
import org.slf4j.Logger;
38+
import org.slf4j.LoggerFactory;
39+
40+
/**
41+
* A command to obsolete an entity and replace all its usages with a replacement
42+
* entity.
43+
* <p>
44+
* This command takes an entity to obsolete and a replacement entity. It will:
45+
* <ul>
46+
* <li>Replace all usages of the obsolete entity with the replacement entity
47+
* throughout the ontology
48+
* <li>Remove the label, comment, and definition from the obsolete entity
49+
* <li>Mark the obsolete entity as deprecated
50+
* <li>Add a new label prefixed with "obsolete" to the obsolete entity
51+
* <li>Add the original label of the obsolete entity as an exact synonym on the
52+
* replacement entity (with a dbxref annotation pointing to the obsolete entity)
53+
* <li>Add a "term replaced by" annotation on the obsolete entity pointing to the
54+
* replacement entity
55+
* </ul>
56+
*/
57+
public class ObsoleteReplaceCommand extends BasePlugin {
58+
59+
private static final Logger logger = LoggerFactory.getLogger(ObsoleteReplaceCommand.class);
60+
61+
public ObsoleteReplaceCommand() {
62+
super("obsolete-replace", "obsolete an entity and replace it with another",
63+
"robot obsolete-replace --obsolete TERM --replacement TERM");
64+
65+
options.addOption(null, "obsolete", true, "entity to obsolete (CURIE or IRI)");
66+
options.addOption(null, "replacement", true, "replacement entity (CURIE or IRI)");
67+
}
68+
69+
@Override
70+
public void performOperation(CommandState state, CommandLine line) throws Exception {
71+
if (!line.hasOption("obsolete") || !line.hasOption("replacement")) {
72+
throw new IllegalArgumentException("Both --obsolete and --replacement options are required");
73+
}
74+
75+
IRI obsoleteIRI = getIRI(line.getOptionValue("obsolete"), "obsolete");
76+
IRI replacementIRI = getIRI(line.getOptionValue("replacement"), "replacement");
77+
78+
OWLOntology ontology = state.getOntology();
79+
OWLOntologyManager manager = ontology.getOWLOntologyManager();
80+
OWLDataFactory factory = manager.getOWLDataFactory();
81+
82+
OWLEntity obsoleteEntity = getEntity(ontology, obsoleteIRI);
83+
if (obsoleteEntity == null) {
84+
throw new IllegalArgumentException("Entity not found: " + obsoleteIRI);
85+
}
86+
87+
OWLEntity replacementEntity = getEntity(ontology, replacementIRI);
88+
if (replacementEntity == null) {
89+
throw new IllegalArgumentException("Replacement entity not found: " + replacementIRI);
90+
}
91+
92+
String originalLabel = getLabel(ontology, obsoleteIRI);
93+
94+
Set<OWLAxiom> axiomsToRemove = new HashSet<>();
95+
for (OWLAnnotationAssertionAxiom axiom : ontology.getAnnotationAssertionAxioms(obsoleteIRI)) {
96+
IRI propertyIRI = axiom.getProperty().getIRI();
97+
if (propertyIRI.equals(Constants.RDFS_LABEL) || propertyIRI.equals(Constants.RDFS_COMMENT)
98+
|| propertyIRI.equals(Constants.IAO_DEFINITION)) {
99+
axiomsToRemove.add(axiom);
100+
}
101+
}
102+
103+
logger.info("Removing {} annotation axioms from obsolete entity", axiomsToRemove.size());
104+
manager.removeAxioms(ontology, axiomsToRemove);
105+
106+
replaceEntity(ontology, obsoleteEntity, replacementEntity);
107+
108+
Set<OWLAxiom> axiomsToAdd = new HashSet<>();
109+
axiomsToAdd.add(factory.getOWLDeclarationAxiom(obsoleteEntity));
110+
axiomsToAdd.add(factory.getOWLAnnotationAssertionAxiom(factory.getOWLDeprecated(), obsoleteIRI,
111+
factory.getOWLLiteral(true)));
112+
113+
String newLabel = "obsolete " + (originalLabel != null ? originalLabel : getLocalName(obsoleteIRI));
114+
axiomsToAdd.add(factory.getOWLAnnotationAssertionAxiom(factory.getRDFSLabel(), obsoleteIRI,
115+
factory.getOWLLiteral(newLabel)));
116+
117+
if (originalLabel != null) {
118+
OWLAnnotationProperty hasExactSynonym = factory.getOWLAnnotationProperty(Constants.HAS_EXACT_SYNONYM);
119+
OWLAnnotationProperty hasDbXref = factory.getOWLAnnotationProperty(Constants.HAS_DBXREF);
120+
OWLAnnotation dbxrefAnnotation = factory.getOWLAnnotation(hasDbXref,
121+
factory.getOWLLiteral(getShortForm(obsoleteIRI)));
122+
Set<OWLAnnotation> synonymAnnotations = Collections.singleton(dbxrefAnnotation);
123+
axiomsToAdd.add(factory.getOWLAnnotationAssertionAxiom(hasExactSynonym, replacementIRI,
124+
factory.getOWLLiteral(originalLabel), synonymAnnotations));
125+
}
126+
127+
OWLAnnotationProperty termReplacedBy = factory.getOWLAnnotationProperty(Constants.TERM_REPLACED_BY);
128+
axiomsToAdd.add(factory.getOWLAnnotationAssertionAxiom(termReplacedBy, obsoleteIRI,
129+
factory.getOWLLiteral(getShortForm(replacementIRI))));
130+
131+
logger.info("Adding {} new axioms to obsolete entity and replacement", axiomsToAdd.size());
132+
manager.addAxioms(ontology, axiomsToAdd);
133+
}
134+
135+
private OWLEntity getEntity(OWLOntology ontology, IRI iri) {
136+
for (OWLEntity entity : ontology.getSignature()) {
137+
if (entity.getIRI().equals(iri)) {
138+
return entity;
139+
}
140+
}
141+
return null;
142+
}
143+
144+
private String getLabel(OWLOntology ontology, IRI entityIRI) {
145+
for (OWLAnnotationAssertionAxiom axiom : ontology.getAnnotationAssertionAxioms(entityIRI)) {
146+
if (axiom.getProperty().isLabel() && axiom.getValue().isLiteral()) {
147+
return axiom.getValue().asLiteral().get().getLiteral();
148+
}
149+
}
150+
return null;
151+
}
152+
153+
private void replaceEntity(OWLOntology ontology, OWLEntity obsolete, OWLEntity replacement) {
154+
OWLOntologyManager manager = ontology.getOWLOntologyManager();
155+
OWLDataFactory factory = manager.getOWLDataFactory();
156+
Set<OWLAxiom> axiomsToRemove = new HashSet<>();
157+
Set<OWLAxiom> axiomsToAdd = new HashSet<>();
158+
159+
for (OWLAxiom axiom : ontology.getAxioms()) {
160+
if (axiom.getSignature().contains(obsolete)) {
161+
axiomsToRemove.add(axiom);
162+
OWLAxiom replacedAxiom = replaceInAxiom(axiom, factory, obsolete, replacement);
163+
axiomsToAdd.add(replacedAxiom);
164+
}
165+
}
166+
167+
logger.info("Replacing {} axioms containing the obsolete entity", axiomsToRemove.size());
168+
manager.removeAxioms(ontology, axiomsToRemove);
169+
manager.addAxioms(ontology, axiomsToAdd);
170+
}
171+
172+
private OWLAxiom replaceInAxiom(OWLAxiom axiom, OWLDataFactory factory, OWLEntity obsolete,
173+
OWLEntity replacement) {
174+
EntityReplacementVisitor visitor = new EntityReplacementVisitor(factory, obsolete, replacement);
175+
axiom.accept(visitor);
176+
return visitor.duplicateObject(axiom);
177+
}
178+
179+
private String getShortForm(IRI iri) {
180+
String iriString = iri.toString();
181+
if (iriString.contains("#")) {
182+
return iriString.substring(iriString.lastIndexOf('#') + 1);
183+
} else if (iriString.contains("/")) {
184+
return iriString.substring(iriString.lastIndexOf('/') + 1);
185+
}
186+
return iriString;
187+
}
188+
189+
private String getLocalName(IRI iri) {
190+
String shortForm = getShortForm(iri);
191+
if (shortForm.contains("_")) {
192+
return shortForm.replace('_', ':');
193+
}
194+
return shortForm;
195+
}
196+
}

src/main/java/org/incenp/obofoundry/odk/StandaloneRobot.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ public static void main(String[] args) {
8484
m.addCommand("validate-profile", new ValidateProfileCommand());
8585
m.addCommand("verify", new VerifyCommand());
8686
m.addCommand("odk:normalize", new NormalizeCommand());
87+
m.addCommand("odk:obsolete-replace", new ObsoleteReplaceCommand());
8788
m.addCommand("odk:subset", new SubsetCommand());
8889
m.addCommand("odk:validate", new CheckAlignmentCommand());
8990

src/main/resources/META-INF/services/org.obolibrary.robot.Command

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ org.incenp.obofoundry.odk.SubsetCommand
33
org.incenp.obofoundry.odk.CheckAlignmentCommand
44
org.incenp.obofoundry.odk.ImportCommand
55
org.incenp.obofoundry.odk.CheckCommand
6+
org.incenp.obofoundry.odk.ObsoleteReplaceCommand
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
Obsoleting and replacing entities
2+
=================================
3+
4+
The `odk:obsolete-replace` command is intended to obsolete an entity and
5+
replace all its usages with a replacement entity in a single operation.
6+
7+
This is useful when merging terms or when an entity needs to be retired
8+
but its usages should be redirected to another term.
9+
10+
Basic usage
11+
-----------
12+
The command requires two options:
13+
14+
* `--obsolete <TERM>`: The entity to obsolete (CURIE or IRI)
15+
* `--replacement <TERM>`: The replacement entity (CURIE or IRI)
16+
17+
Example:
18+
19+
```
20+
robot odk:obsolete-replace --input ontology.owl \
21+
--obsolete EXAMPLE:0000001 \
22+
--replacement EXAMPLE:0000002 \
23+
--output output.owl
24+
```
25+
26+
What the command does
27+
---------------------
28+
When executed, the command performs the following operations:
29+
30+
1. **Replaces all usages** of the obsolete entity with the replacement
31+
entity throughout the ontology (in all axioms)
32+
2. **Removes annotations** from the obsolete entity: label, comment, and
33+
definition (IAO:0000115)
34+
3. **Marks the obsolete entity as deprecated** by adding an
35+
`owl:deprecated true` annotation
36+
4. **Adds a new label** to the obsolete entity, prefixed with "obsolete"
37+
(e.g., "obsolete old term name")
38+
5. **Adds the original label as an exact synonym** on the replacement
39+
entity, with a database cross-reference annotation pointing to the
40+
obsolete entity
41+
6. **Adds a "term replaced by" annotation** (IAO:0100001) on the
42+
obsolete entity pointing to the replacement entity
43+
44+
Example workflow
45+
----------------
46+
A typical workflow might look like:
47+
48+
```
49+
robot merge --input ontology.owl \
50+
odk:obsolete-replace --obsolete GO:0000001 \
51+
--replacement GO:0000002 \
52+
annotate --ontology-iri http://example.org/ontology.owl \
53+
--output output.owl
54+
```
55+
56+
This merges the ontology, obsoletes `GO:0000001` replacing it with
57+
`GO:0000002`, and then annotates and saves the result.

0 commit comments

Comments
 (0)