diff --git a/robot/src/main/java/org/incenp/obofoundry/kgcl/robot/MintCommand.java b/robot/src/main/java/org/incenp/obofoundry/kgcl/robot/MintCommand.java
new file mode 100644
index 0000000..a0e07c1
--- /dev/null
+++ b/robot/src/main/java/org/incenp/obofoundry/kgcl/robot/MintCommand.java
@@ -0,0 +1,226 @@
+/*
+ * 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 .
+ */
+
+package org.incenp.obofoundry.kgcl.robot;
+
+import java.util.HashMap;
+import java.util.HashSet;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.Options;
+import org.apache.commons.compress.utils.Sets;
+import org.incenp.obofoundry.kgcl.IAutoIDGenerator;
+import org.incenp.obofoundry.kgcl.SequentialIDGenerator;
+import org.obolibrary.robot.Command;
+import org.obolibrary.robot.CommandLineHelper;
+import org.obolibrary.robot.CommandState;
+import org.obolibrary.robot.IOHelper;
+import org.semanticweb.owlapi.model.IRI;
+import org.semanticweb.owlapi.model.OWLAnnotationAssertionAxiom;
+import org.semanticweb.owlapi.model.OWLAnnotationProperty;
+import org.semanticweb.owlapi.model.OWLAxiom;
+import org.semanticweb.owlapi.model.OWLDataFactory;
+import org.semanticweb.owlapi.model.OWLEntity;
+import org.semanticweb.owlapi.model.OWLLiteral;
+import org.semanticweb.owlapi.model.OWLOntology;
+import org.semanticweb.owlapi.model.OWLOntologyManager;
+import org.semanticweb.owlapi.util.OWLEntityRenamer;
+import org.semanticweb.owlapi.vocab.OWLRDFVocabulary;
+
+public class MintCommand implements Command {
+
+ private static final String DEFAULT_TEMP_PREFIX = "http://purl.obolibrary.org/temp#";
+ private static final String REPLACED_BY = "http://purl.obolibrary.org/obo/IAO_0100001";
+
+ private Options options;
+
+ public MintCommand() {
+ options = CommandLineHelper.getCommonOptions();
+ options.addOption("i", "input", true, "load ontology from file");
+ options.addOption("o", "output", true, "save ontology to file");
+
+ options.addOption(null, "temp-id-prefix", true, "prefix of temporary identifiers to replace");
+
+ options.addOption(null, "keep-deprecated", false, "keep temporary terms as deprecated entities");
+ options.addOption(null, "minted-from-property", true,
+ "property used to link minted identifiers to temporary identifiers");
+
+ options.addOption(null, "minted-id-prefix", true, "prefix of newly minted identifiers");
+ options.addOption(null, "pad-width", true, "pad the numerical portion of minted identifiers up to this width");
+ options.addOption(null, "min-id", true, "lower bound of the range for newly minted identifiers");
+ options.addOption(null, "max-id", true, "upper bound of the range for newly minted identifiers");
+
+ options.addOption(null, "id-range-file", true, "Use the specified ID range file");
+ options.addOption(null, "id-range-name", true, "Use the specified ID range name");
+ }
+
+ @Override
+ public String getName() {
+ return "mint";
+ }
+
+ @Override
+ public String getDescription() {
+ return "replace temporary identifiers by newly minted permanent identifiers";
+ }
+
+ @Override
+ public String getUsage() {
+ return "robot mint --input --temp-id-prefix --keep-deprecated\n"
+ + " --minted-from-property ] --minted-id-prefix \n"
+ + " --pad-width --min-id --max-id \n"
+ + " --id-range-file --id-range-name \n";
+ }
+
+ @Override
+ public Options getOptions() {
+ return options;
+ }
+
+ @Override
+ public void main(String[] args) {
+ try {
+ execute(null, args);
+ } catch ( Exception e ) {
+ CommandLineHelper.handleException(e);
+ }
+ }
+
+ @Override
+ public CommandState execute(CommandState state, String[] args) throws Exception {
+ CommandLine line = CommandLineHelper.getCommandLine(getUsage(), options, args);
+ if ( line == null ) {
+ return null;
+ }
+
+ IOHelper ioHelper = CommandLineHelper.getIOHelper(line);
+ state = CommandLineHelper.updateInputOntology(ioHelper, state, line);
+
+ OWLOntology ontology = state.getOntology();
+ OWLOntologyManager manager = ontology.getOWLOntologyManager();
+ OWLDataFactory factory = manager.getOWLDataFactory();
+ OWLEntityRenamer renamer = new OWLEntityRenamer(ontology.getOWLOntologyManager(), Sets.newHashSet(ontology));
+
+ // Annotation properties we need for --minted-from and --keep-deprecated
+ OWLAnnotationProperty labelProp = factory.getOWLAnnotationProperty(OWLRDFVocabulary.RDFS_LABEL.getIRI());
+ OWLAnnotationProperty replacedByProp = factory.getOWLAnnotationProperty(IRI.create(REPLACED_BY));
+ OWLAnnotationProperty deprecatedProp = factory
+ .getOWLAnnotationProperty(OWLRDFVocabulary.OWL_DEPRECATED.getIRI());
+ OWLAnnotationProperty mintedFromProp = null;
+ if ( line.hasOption("minted-from-property") ) {
+ mintedFromProp = factory.getOWLAnnotationProperty(IRI.create(line.getOptionValue("minted-from-property")));
+ }
+
+ String tempPrefix = line.getOptionValue("temp-id-prefix", DEFAULT_TEMP_PREFIX);
+ IAutoIDGenerator generator = getAutoIDGenerator(line, ontology);
+ if ( generator == null ) {
+ throw new Exception("No ID generator set, cannot mint new IDs");
+ }
+ boolean keepDeprecated = line.hasOption("keep-deprecated");
+
+ // Collect IDs that needs to be replaced.
+ HashMap ids = new HashMap();
+ HashMap savedLabels = new HashMap();
+ HashSet savedAxioms = new HashSet();
+ for ( OWLEntity entity : ontology.getSignature() ) {
+ String origIRI = entity.getIRI().toString();
+ if ( origIRI.startsWith(tempPrefix) ) {
+ String newIRI = generator.nextID();
+ if ( newIRI == null ) {
+ throw new Exception("Cannot mint new ID");
+ }
+
+ ids.put(origIRI, newIRI);
+
+ if ( keepDeprecated ) {
+ // Keep aside the original declaration axioms and labels
+ savedAxioms.addAll(ontology.getDeclarationAxioms(entity));
+ for ( OWLAnnotationAssertionAxiom ax : ontology.getAnnotationAssertionAxioms(entity.getIRI()) ) {
+ if ( ax.getProperty().isLabel() && ax.getValue().isLiteral() ) {
+ savedLabels.put(origIRI, ax.getValue().asLiteral().get());
+ }
+ }
+ }
+ }
+ }
+
+ // Proceed with replacement for the IDs we found
+ for ( String oldID : ids.keySet() ) {
+ IRI oldIRI = IRI.create(oldID);
+ IRI newIRI = IRI.create(ids.get(oldID));
+
+ // Actual renaming
+ manager.applyChanges(renamer.changeIRI(oldIRI, newIRI));
+
+ // Add a "minted-from" annotation?
+ if ( mintedFromProp != null ) {
+ manager.addAxiom(ontology, factory.getOWLAnnotationAssertionAxiom(mintedFromProp, newIRI, oldIRI));
+ }
+
+ // Keep the original entities as deprecated entities?
+ if ( keepDeprecated ) {
+ manager.addAxiom(ontology,
+ factory.getOWLAnnotationAssertionAxiom(deprecatedProp, oldIRI, factory.getOWLLiteral(true)));
+ manager.addAxiom(ontology, factory.getOWLAnnotationAssertionAxiom(replacedByProp, oldIRI, newIRI));
+ OWLLiteral label = savedLabels.get(oldID);
+ if ( label != null ) {
+ // Prepend "obsolete " if the label is English or language-neutral
+ if ( label.getLang().isEmpty() || label.getLang().equalsIgnoreCase("en") ) {
+ label = factory.getOWLLiteral("obsolete " + label.getLiteral(), label.getLang());
+ }
+ manager.addAxiom(ontology, factory.getOWLAnnotationAssertionAxiom(labelProp, oldIRI, label));
+ }
+ }
+ }
+
+ // --keep-deprecated: restore the declaration axioms for the deprecated entities
+ if ( !savedAxioms.isEmpty() ) {
+ manager.addAxioms(ontology, savedAxioms);
+ }
+
+ CommandLineHelper.maybeSaveOutput(line, ontology);
+
+ return state;
+ }
+
+ private IAutoIDGenerator getAutoIDGenerator(CommandLine line, OWLOntology ontology) throws Exception {
+ if ( line.hasOption("minted-id-prefix") ) {
+ /*
+ * Manual mode; generate IDs in a range that is explicitly specified on the
+ * command line.
+ */
+ if ( !line.hasOption("min-id") ) {
+ throw new Exception("Missing --min-id option for auto-assigned IDs");
+ }
+ int lower = Integer.parseInt(line.getOptionValue("min-id"));
+ int upper = line.hasOption("max-id") ? Integer.parseInt(line.getOptionValue("max-id")) : lower + 1000;
+ int width = line.hasOption("pad-width") ? Integer.parseInt(line.getOptionValue("pad-width")) : 7;
+ String format = String.format("%s%%0%dd", line.getOptionValue("minted-id-prefix"), width);
+ return new SequentialIDGenerator(ontology, format, lower, upper);
+ } else {
+ /*
+ * Range-file mode; similar, but the range is obtained from a file of ID ranges.
+ */
+ String rangeFile = line.getOptionValue("id-range-file");
+ String requestedName = line.getOptionValue("id-range-name");
+ String[] defaultNames = new String[] { "auto-minter" };
+ return IDRangeHelper.maybeGetIDGenerator(ontology, rangeFile, requestedName, defaultNames, false);
+ }
+ }
+
+}
diff --git a/robot/src/main/java/org/incenp/obofoundry/kgcl/robot/StandaloneRobot.java b/robot/src/main/java/org/incenp/obofoundry/kgcl/robot/StandaloneRobot.java
index edee748..fb19ea4 100644
--- a/robot/src/main/java/org/incenp/obofoundry/kgcl/robot/StandaloneRobot.java
+++ b/robot/src/main/java/org/incenp/obofoundry/kgcl/robot/StandaloneRobot.java
@@ -85,6 +85,7 @@ public static void main(String[] args) {
m.addCommand("verify", new VerifyCommand());
m.addCommand("kgcl-apply", new ApplyCommand());
+ m.addCommand("kgcl-mint", new MintCommand());
new PluginManager().addPluggableCommands(m);
diff --git a/robot/src/main/resources/META-INF/services/org.obolibrary.robot.Command b/robot/src/main/resources/META-INF/services/org.obolibrary.robot.Command
index 9bfae9b..1d5bf77 100644
--- a/robot/src/main/resources/META-INF/services/org.obolibrary.robot.Command
+++ b/robot/src/main/resources/META-INF/services/org.obolibrary.robot.Command
@@ -1 +1,2 @@
org.incenp.obofoundry.kgcl.robot.ApplyCommand
+org.incenp.obofoundry.kgcl.robot.MintCommand