diff --git a/.gitignore b/.gitignore index e21e7f888..e65649a99 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .project .settings .idea +.vscode *.iml Gemfile* bin/original-robot.jar diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fe7129b6..8b3ff924c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- '--annotate-with-source true' does not work with extract --method subset [#1160] +- Fix how Template adds entities to the QuotedEntityChecker [#1104] +- [`merge`] and 'annotate' operations '--annotate-defined-by' excludes reserved OWL 2 vocabularies [#1171] +- Handle IRIs that are not entities in export [#1168] + +## [1.9.5] - 2023-09-20 + ### Added +- Updated ELK from 0.4.3 to 0.5.0. [#999]. This is an important change as ELK 0.5.0 is more complete than 0.4.3, which means that it will potentially uncover inferences, in particular unsatisfiable classes, which were not recognised by ELK 0.4.3. +- Add support for pluggable commands [#1119] - Add `--drop-axiom-annotations` option to drop axiom annotations in [`remove`] and [`filter`] [#1023] +### Changed +- Migrate to OWL API 4.5.26 to deal with [broken turtle serialiser](https://github.com/ontodev/robot/issues/1129). [#1135] +- Improvements to `export` and `report` for XLSX format [#1148] + +## [1.9.4] - 2023-05-23 + +### Changed +- Speed up unsatisfiable object-property check on certain reasoners including HermiT [#1100] +- Update HermiT from 1.3.8.413 to 1.4.5.456 [#1073] + +### Fixed +- Preserve prefixes across ontology load and save [#1101] + ## [1.9.3] - 2023-02-16 ### Added @@ -311,7 +334,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 First official release of ROBOT! -[Unreleased]: https://github.com/ontodev/robot/compare/v1.9.3...HEAD +[Unreleased]: https://github.com/ontodev/robot/compare/v1.9.5...HEAD +[1.9.5]: https://github.com/ontodev/robot/compare/v1.9.4...v1.9.5 +[1.9.4]: https://github.com/ontodev/robot/compare/v1.9.3...v1.9.4 [1.9.3]: https://github.com/ontodev/robot/compare/v1.9.2...v1.9.3 [1.9.2]: https://github.com/ontodev/robot/compare/v1.9.1...v1.9.2 [1.9.1]: https://github.com/ontodev/robot/compare/v1.9.0...v1.9.1 @@ -359,11 +384,20 @@ First official release of ROBOT! [`validate`]: http://robot.obolibrary.org/validate [#1023]: https://github.com/ontodev/robot/pull/1023 +[#1171]: https://github.com/ontodev/robot/pull/1171 +[#1168]: https://github.com/ontodev/robot/pull/1168 +[#1160]: https://github.com/ontodev/robot/pull/1160 +[#1148]: https://github.com/ontodev/robot/pull/1148 +[#1135]: https://github.com/ontodev/robot/pull/1135 +[#1119]: https://github.com/ontodev/robot/pull/1119 +[#1104]: https://github.com/ontodev/robot/pull/1104 +[#1100]: https://github.com/ontodev/robot/pull/1100 [#1091]: https://github.com/ontodev/robot/issues/1091 [#1089]: https://github.com/ontodev/robot/issues/1089 [#1088]: https://github.com/ontodev/robot/issues/1088 [#1086]: https://github.com/ontodev/robot/pull/1086 [#1084]: https://github.com/ontodev/robot/issues/1084 +[#1073]: https://github.com/ontodev/robot/pull/1073 [#1071]: https://github.com/ontodev/robot/pull/1071 [#1061]: https://github.com/ontodev/robot/issues/1061 [#1030]: https://github.com/ontodev/robot/issues/1030 @@ -372,6 +406,7 @@ First official release of ROBOT! [#1016]: https://github.com/ontodev/robot/issues/1016 [#1009]: https://github.com/ontodev/robot/issues/1009 [#1000]: https://github.com/ontodev/robot/pull/1000 +[#999]: https://github.com/ontodev/robot/pull/999 [#979]: https://github.com/ontodev/robot/pull/979 [#978]: https://github.com/ontodev/robot/pull/978 [#977]: https://github.com/ontodev/robot/pull/977 diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index e5da5015a..2277aa56e 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -27,6 +27,7 @@ chaining commands
global options
makefile
+ plugins
- - - - - - - - - -
annotate
collapse
diff --git a/docs/annotate.md b/docs/annotate.md index 01a20da0a..79f7dd41e 100644 --- a/docs/annotate.md +++ b/docs/annotate.md @@ -10,7 +10,7 @@ General annotations can be added one-by-one with `--annotation`, and the IRIs ca --annotation rdfs:comment "Comment" \ --annotation rdfs:label "Label" \ --annotation-file annotations.ttl \ - --output results/edit-annotated.owl + --output results/edi-annotated.owl Or all at once from a turtle (.ttl) file with `--annotation-file`: diff --git a/docs/examples/edit-annotated.owl b/docs/examples/edi-annotated.owl similarity index 100% rename from docs/examples/edit-annotated.owl rename to docs/examples/edi-annotated.owl diff --git a/docs/examples/example2_defined_by.owl b/docs/examples/example2_defined_by.owl index d15c175ac..82bb72339 100644 --- a/docs/examples/example2_defined_by.owl +++ b/docs/examples/example2_defined_by.owl @@ -78,62 +78,12 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -284,18 +234,6 @@ dummy individal 5 - - - - - - diff --git a/docs/examples/merged_defined_by.owl b/docs/examples/merged_defined_by.owl index d988d4fa2..e71912c17 100644 --- a/docs/examples/merged_defined_by.owl +++ b/docs/examples/merged_defined_by.owl @@ -78,54 +78,12 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/examples/subset_result.owl b/docs/examples/subset_result.owl index d79a8139b..49e751c26 100644 --- a/docs/examples/subset_result.owl +++ b/docs/examples/subset_result.owl @@ -25,21 +25,23 @@ - database_cross_reference + database_cross_reference - + + id + - in_subset + in_subset @@ -47,7 +49,7 @@ - shorthand + shorthand @@ -66,10 +68,10 @@ - - BFO:0000050 - part_of - part_of + + BFO:0000050 + part_of + part_of @@ -88,7 +90,7 @@ - ONT:1 + ONT:1 @@ -103,11 +105,12 @@ - ONT:5 + ONT:5 - \ No newline at end of file + + diff --git a/docs/extract.md b/docs/extract.md index da832212d..0ef88d43e 100644 --- a/docs/extract.md +++ b/docs/extract.md @@ -79,12 +79,14 @@ For more details see the [MIREOT paper](http://dx.doi.org/10.3233/AO-2011-0087). The subset method extracts a sub-ontology that contains only the seed terms (that you specify with `--term` and `--term-file` options) and the relations between them. This method uses the [relation-graph](https://github.com/balhoff/relation-graph) to materialize the existential relations among the seed terms. Procedurally, the subset method materializes the input ontology and adds the inferred axioms to the input ontology. Then filters the ontology with the given seed terms. Finally, it reduces the filtered ontology to remove redundant subClassOf axioms. - robot extract --method subset \ - --input subset.obo \ - --term "obo:ONT_1" \ - --term "obo:ONT_5" \ - --term "BFO:0000050" \ - --output results/subset_result.owl +``` +robot extract --method subset \ + --input subset.obo \ + --term "obo:ONT_1" \ + --term "obo:ONT_5" \ + --term "BFO:0000050" \ + --output results/subset_result.owl +``` ROBOT expects any `--term` or IRI in the `--term-file` to exist in the input ontology. If none of the input terms exist, the command will fail with an [empty terms error](errors#empty-terms-error). This can be overridden by including `--force true`. diff --git a/docs/index.md b/docs/index.md index 84f79a287..7e77924d3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -39,7 +39,8 @@ The command-line tool is packaged a Java JAR file and can be run via the `robot` 1. Download the `robot.jar` file from the [latest release](https://github.com/ontodev/robot/releases/latest). 2. Save the [ROBOT batch script](https://github.com/ontodev/robot/raw/master/bin/robot.bat). - Make sure this is saved as `.bat` and not `.bat.txt` - - OR enter `"java %ROBOT_JAVA_ARGS% -jar %~dr0robot.jar %*" | out-file robot.bat -encoding utf8` in the same directory as `robot.jar` to create the batch script. + - OR in [PowerShell](https://learn.microsoft.com/powershell/), run `"java %ROBOT_JAVA_ARGS% -jar %~dp0robot.jar %*" | out-file robot.bat -encoding utf8` in the same directory as `robot.jar` to create the batch script. + - Note that the above command requires PowerShell version 6 or later: previous versions will write a Unicode byte order mark (BOM) to the file, which breaks the command. 3. Put both files on your [system PATH](https://en.wikipedia.org/wiki/PATH_(variable)) in the same directory. - this could be `C:\Windows\` - OR [update your PATH](https://docs.oracle.com/javase/tutorial/essential/environment/paths.html) to include the new directory. diff --git a/docs/merge.md b/docs/merge.md index b8d1a80c8..d74f96ea8 100644 --- a/docs/merge.md +++ b/docs/merge.md @@ -46,3 +46,5 @@ It’s also possible to annotate the imported or merged ontology axioms with the robot merge --input example2.owl --input merge.owl \ --annotate-defined-by true \ --output results/merged_defined_by.owl + +`--annotate-defined-by` excludes entities from the reserved OWL 2 vocabularies (RDF, RDFS, XSD and OWL). diff --git a/docs/plugins.md b/docs/plugins.md new file mode 100644 index 000000000..f21703dd8 --- /dev/null +++ b/docs/plugins.md @@ -0,0 +1,36 @@ +# Plugins + +The set of ROBOT commands can be extended locally with plugins. A ROBOT plugin is a Java archive file (`.jar`) providing one or more supplementary commands (hereafter called "pluggable commands"). + +## Using plugins + +ROBOT searches for plugins in the following locations: + +* the `.robot/plugins` directory in the current user's home directory; +* the directory specified by the Java system property `robot.pluginsdir`, if such a property is set; +* the directory specified by the environment variable `ROBOT_PLUGINS_DIRECTORY`, if such a variable is set in the environment. + +Installing a plugin is therefore simply a matter of either + +* placing the Jar file into your `~/.robot/plugins` directory, or +* placing the Jar file into any directory and making sure ROBOT knows to search that directory, by setting the `robot.pluginsdir` system property or the `ROBOT_PLUGINS_DIRECTORY` environment variable accordingly. + +Importantly, the basename of the Jar file (without the `.jar` extension) within the directory will become part of the name of any pluggable command provided by the plugin. For example, if the file is named `myplugin.jar` and it provides a command called `mycommand`, that command will be available under the name `myplugin:mycommand`. Because of that: + +* the name of the Jar file **must** be in lowercase only; +* the name **should** be kept short and simple. + +Once the plugin is installed, any pluggable command it provides is immediately available to ROBOT. You can check by calling `robot` without any argument to get it to print the full list of available commands, which will include the commands provided by installed plugins, if any. + +## Creating plugins + +A pluggable command, just like any other ROBOT command, is a Java class that implements the `org.obolibrary.robot.Command` interface. A plugin is Java archive file that contains at least: + +* the compiled Java code ("bytecode") for at least one class implementing the `org.obolibrary.robot.Command` interface, and +* a `META-INF/services/org.obolibrary.robot.Command` file that list all implementations of that interface available in the archive (one per line). + +For example, if the command `mycommand` is implemented in a class named `MyCommand` in the package `org.example.myplugin`, the `META-INF/services/org.obolibrary.robot.Command` file must contain a single line `org.example.myplugin.MyCommand`. + +In addition to the class implementing the command itself, the archive must also provide any additional classes that may be required for the command to work. This must include classes from any external dependency, unless that dependency also happens to be a dependency of ROBOT itself (for example, there is no need for the archive to contain a copy of the classes of the OWL API, since they are already present in the standard distribution of ROBOT). + +A more detailed walkthrough of how to create a plugin is available [here](https://incenp.org/notes/2023/writing-robot-plugins.html). diff --git a/docs/rename.md b/docs/rename.md index e5b72da4a..3603db632 100644 --- a/docs/rename.md +++ b/docs/rename.md @@ -54,8 +54,8 @@ The mappings for renaming should be specified with the `--mappings` (for full re For a full rename (you can use prefixes as long as they are defined by the defaults, `--prefix`, or `--add-prefix`): ```tsv -Old IRI New IRI -obo:BFO_0000051 fb:BFO_1234567 +Old IRI New IRI +obo:BFO_0000051 fb:BFO_1234567 ``` If you also want to update the `rdfs:label` of the term you replaced, you can add a third column with the new label value (note that this removes *all* old label annotations): @@ -68,8 +68,8 @@ obo:BFO_0000051 fb:BFO_1234567 foo bar For a prefix rename: ```tsv -Old Base New Base -http://purl.obolibrary.org/obo/ http://foo.bar/ +Old Base New Base +http://purl.obolibrary.org/obo/ http://foo.bar/ ``` The `rename` command expects the first line to contain headers. diff --git a/docs/report_queries/multiple_definitions.md b/docs/report_queries/multiple_definitions.md index 0079330f8..baeefd863 100644 --- a/docs/report_queries/multiple_definitions.md +++ b/docs/report_queries/multiple_definitions.md @@ -1,4 +1,4 @@ -# Multiple Defintions +# Multiple Definitions **Problem:** An entity has more than one definition or elucidation. This may cause confusion or misuse, and will prevent translation to OBO format (in case of multiple definitions). Excludes deprecated entities. diff --git a/pom.xml b/pom.xml index afb83798c..ecbf1494d 100644 --- a/pom.xml +++ b/pom.xml @@ -69,6 +69,7 @@ robot-core robot-command robot-maven-plugin + robot-mock-plugin @@ -165,7 +166,7 @@ net.sourceforge.owlapi owlapi-api - 4.5.25 + 4.5.26 com.google.code.findbugs @@ -176,7 +177,7 @@ net.sourceforge.owlapi owlapi-apibinding - 4.5.25 + 4.5.26 com.google.code.findbugs @@ -187,7 +188,7 @@ net.sourceforge.owlapi owlapi-rio - 4.5.25 + 4.5.26 com.google.code.findbugs @@ -203,7 +204,7 @@ ch.qos.logback logback-classic - 1.2.7 + 1.2.13 org.slf4j @@ -211,9 +212,9 @@ 1.7.32 - org.semanticweb.elk - elk-owlapi - 0.4.3 + au.csiro + elk-owlapi4 + 0.5.0 log4j @@ -236,7 +237,7 @@ net.sourceforge.owlapi org.semanticweb.hermit - 1.3.8.413 + 1.4.5.456 net.sourceforge.owlapi diff --git a/robot-command/src/main/java/org/obolibrary/robot/AnnotateCommand.java b/robot-command/src/main/java/org/obolibrary/robot/AnnotateCommand.java index 8a9d3499b..e35fc36b0 100644 --- a/robot-command/src/main/java/org/obolibrary/robot/AnnotateCommand.java +++ b/robot-command/src/main/java/org/obolibrary/robot/AnnotateCommand.java @@ -318,7 +318,9 @@ public CommandState execute(CommandState state, String[] args) throws Exception OWLAnnotationProperty rdfsIsDefinedBy = ontology.getOWLOntologyManager().getOWLDataFactory().getRDFSIsDefinedBy(); for (OWLEntity owlEntity : ontology.getSignature()) { - OntologyHelper.addEntityAnnotation(ontology, owlEntity, rdfsIsDefinedBy, ontIRI, false); + if (!owlEntity.getIRI().isReservedVocabulary()) { + OntologyHelper.addEntityAnnotation(ontology, owlEntity, rdfsIsDefinedBy, ontIRI, false); + } } } } diff --git a/robot-command/src/main/java/org/obolibrary/robot/CommandLineInterface.java b/robot-command/src/main/java/org/obolibrary/robot/CommandLineInterface.java index f902bf276..a6c218f30 100644 --- a/robot-command/src/main/java/org/obolibrary/robot/CommandLineInterface.java +++ b/robot-command/src/main/java/org/obolibrary/robot/CommandLineInterface.java @@ -46,6 +46,10 @@ private static CommandManager initManager() { m.addCommand("unmerge", new UnmergeCommand()); m.addCommand("validate-profile", new ValidateProfileCommand()); m.addCommand("verify", new VerifyCommand()); + + PluginManager pm = new PluginManager(); + pm.addPluggableCommands(m); + return m; } diff --git a/robot-command/src/main/java/org/obolibrary/robot/FilterCommand.java b/robot-command/src/main/java/org/obolibrary/robot/FilterCommand.java index a38cb39ff..3c1b65d37 100644 --- a/robot-command/src/main/java/org/obolibrary/robot/FilterCommand.java +++ b/robot-command/src/main/java/org/obolibrary/robot/FilterCommand.java @@ -171,7 +171,8 @@ public CommandState execute(CommandState state, String[] args) throws Exception dropParameters.stream() .filter(s -> !s.equalsIgnoreCase("all")) .map( - curie -> CommandLineHelper.maybeCreateIRI(ioHelper, curie, "drop-axiom-annotations")) + curie -> + CommandLineHelper.maybeCreateIRI(ioHelper, curie, "drop-axiom-annotations")) .collect(Collectors.toList()); // Use the select statements to get a set of objects to filter diff --git a/robot-command/src/main/java/org/obolibrary/robot/PluginManager.java b/robot-command/src/main/java/org/obolibrary/robot/PluginManager.java new file mode 100644 index 000000000..bb0a937e0 --- /dev/null +++ b/robot-command/src/main/java/org/obolibrary/robot/PluginManager.java @@ -0,0 +1,106 @@ +package org.obolibrary.robot; + +import java.io.File; +import java.io.FilenameFilter; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.HashMap; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Pluggable commands loader. + * + * @author Damien Goutte-Gattat + */ +public class PluginManager { + + private static final Logger logger = LoggerFactory.getLogger(PluginManager.class); + + private HashMap jars = null; + + /** + * Find pluggable commands and add them to a CommandManager. + * + * @param cm the command manager to add commands to + */ + public void addPluggableCommands(CommandManager cm) { + if (jars == null) { + findPlugins(); + } + + loadPlugin(cm, null, ""); + for (String pluginBasename : jars.keySet()) { + loadPlugin(cm, jars.get(pluginBasename), pluginBasename + ":"); + } + } + + /** + * Load pluggable commands from a Jar file. + * + * @param cm the command manager to add commands to + * @param jarFile the Jar file to load commands from; if null, will attempt to find pluggable + * commands in the system class path + * @param prefix a string to prepend to the name of each pluggable command when adding them to the + * command manager + */ + private void loadPlugin(CommandManager cm, URL jarFile, String prefix) { + ClassLoader classLoader = + jarFile != null + ? URLClassLoader.newInstance(new URL[] {jarFile}) + : URLClassLoader.getSystemClassLoader(); + + try { + ServiceLoader serviceLoader = ServiceLoader.load(Command.class, classLoader); + for (Command pluggableCommand : serviceLoader) { + cm.addCommand(prefix + pluggableCommand.getName(), pluggableCommand); + } + } catch (ServiceConfigurationError e) { + logger.warn("Invalid configuration in plugin %s, ignoring plugin", jarFile); + } + } + + /** + * Detect Jar files in a set of directories. If a Jar file with the same basename is found in more + * than one directory, the last one found takes precedence. + */ + private void findPlugins() { + String[] pluginsDirectories = { + System.getProperty("robot.pluginsdir"), + System.getenv("ROBOT_PLUGINS_DIRECTORY"), + new File(System.getProperty("user.home"), ".robot/plugins").getPath() + }; + FilenameFilter jarFilter = + new FilenameFilter() { + @Override + public boolean accept(File file, String name) { + return name.endsWith(".jar"); + } + }; + + jars = new HashMap(); + + for (String directoryName : pluginsDirectories) { + if (directoryName == null || directoryName.length() == 0) { + continue; + } + + File directory = new File(directoryName); + if (directory.isDirectory()) { + for (File jarFile : directory.listFiles(jarFilter)) { + try { + String basename = jarFile.getName(); + basename = basename.substring(0, basename.length() - 4); + jars.put(basename, jarFile.toURI().toURL()); + } catch (MalformedURLException e) { + // This should never happen: the URL is constructed by the Java Class Library + // from a real filename, it should never be malformed. + } + } + } + } + } +} diff --git a/robot-command/src/main/java/org/obolibrary/robot/RemoveCommand.java b/robot-command/src/main/java/org/obolibrary/robot/RemoveCommand.java index d8ec1f8a9..81905de60 100644 --- a/robot-command/src/main/java/org/obolibrary/robot/RemoveCommand.java +++ b/robot-command/src/main/java/org/obolibrary/robot/RemoveCommand.java @@ -161,7 +161,8 @@ public CommandState execute(CommandState state, String[] args) throws Exception dropParameters.stream() .filter(s -> !s.equalsIgnoreCase("all")) .map( - curie -> CommandLineHelper.maybeCreateIRI(ioHelper, curie, "drop-axiom-annotations")) + curie -> + CommandLineHelper.maybeCreateIRI(ioHelper, curie, "drop-axiom-annotations")) .collect(Collectors.toList()); // Get the objects to remove diff --git a/robot-core/pom.xml b/robot-core/pom.xml index 2e7258616..9f94fa0b0 100644 --- a/robot-core/pom.xml +++ b/robot-core/pom.xml @@ -42,7 +42,7 @@ ${project.groupId} ${project.artifactId} - 1.9.3 + 1.9.5 jar @@ -86,7 +86,7 @@ net.sourceforge.owlapi owlapi-api - 4.5.25 + 4.5.26 com.google.code.findbugs @@ -97,7 +97,7 @@ net.sourceforge.owlapi owlapi-apibinding - 4.5.25 + 4.5.26 com.google.code.findbugs @@ -108,7 +108,7 @@ net.sourceforge.owlapi owlapi-rio - 4.5.25 + 4.5.26 com.google.code.findbugs @@ -301,7 +301,7 @@ org.geneontology relation-graph_${scala.version} - 2.2.1 + 2.3.2 org.apache.jena diff --git a/robot-core/src/main/java/org/obolibrary/robot/ExportOperation.java b/robot-core/src/main/java/org/obolibrary/robot/ExportOperation.java index 009f1d72f..2a10711ee 100644 --- a/robot-core/src/main/java/org/obolibrary/robot/ExportOperation.java +++ b/robot-core/src/main/java/org/obolibrary/robot/ExportOperation.java @@ -496,8 +496,12 @@ private static List getPropertyValues( IRI iri = a.getValue().asIRI().orNull(); if (iri != null) { Set entities = ontology.getEntitiesInSignature(iri); - for (OWLEntity e : entities) { - values.add(OntologyHelper.renderManchester(e, provider, rt)); + if (entities.size() > 0) { + for (OWLEntity e : entities) { + values.add(OntologyHelper.renderManchester(e, provider, rt)); + } + } else { + values.add(iri.toString()); } } } else { @@ -724,6 +728,11 @@ private static Set getRestrictionFillers( } } break; + // TODO + case DATA_HAS_VALUE: + break; + default: + break; } } return fillers; @@ -831,10 +840,25 @@ private static Set getRestrictionFillers( } } break; + // TODO + case OBJECT_HAS_VALUE: + // property value instance + case OBJECT_COMPLEMENT_OF: + // not ce + case OBJECT_HAS_SELF: + // property Self + // this requires the subject entity to render case OBJECT_ONE_OF: + // {instance, instance,...} + // this should not occur case OBJECT_UNION_OF: + // ce or ce or ce + // this should not occur case OBJECT_INTERSECTION_OF: - // TODO + // ce and ce and ce + // this should not occur + break; + default: break; } } @@ -1095,18 +1119,8 @@ private static Row getRow(OWLOntology ontology, Table table, OWLEntity entity) t case "http://www.w3.org/2002/07/owl#equivalentProperty": // Equivalent Properties if (entity.isOWLAnnotationProperty()) { - Collection eqs = - EntitySearcher.getEquivalentProperties(entity.asOWLAnnotationProperty(), ontology); - row.add( - getObjectCell( - eqs, - col, - displayRendererType, - sortRendererType, - provider, - includeNamed, - includeAnonymous)); - + // no equivalent annotation property in OWL + break; } else if (entity.isOWLDataProperty()) { Collection eqs = EntitySearcher.getEquivalentProperties(entity.asOWLDataProperty(), ontology); @@ -1140,6 +1154,8 @@ private static Row getRow(OWLOntology ontology, Table table, OWLEntity entity) t if (entity.isOWLClass()) { Collection disjoints = EntitySearcher.getDisjointClasses(entity.asOWLClass(), ontology); + // remove self-disjoint + disjoints.remove(entity.asOWLClass()); row.add( getObjectCell( disjoints, @@ -1153,6 +1169,8 @@ private static Row getRow(OWLOntology ontology, Table table, OWLEntity entity) t } else if (entity.isOWLDataProperty()) { Collection disjoints = EntitySearcher.getDisjointProperties(entity.asOWLDataProperty(), ontology); + // remove self-disjoint + disjoints.remove(entity.asOWLDataProperty()); row.add( getObjectCell( disjoints, @@ -1166,6 +1184,8 @@ private static Row getRow(OWLOntology ontology, Table table, OWLEntity entity) t } else if (entity.isOWLObjectProperty()) { Collection disjoints = EntitySearcher.getDisjointProperties(entity.asOWLObjectProperty(), ontology); + // remove self-disjoint + disjoints.remove(entity.asOWLObjectProperty()); row.add( getObjectCell( disjoints, diff --git a/robot-core/src/main/java/org/obolibrary/robot/ExtractOperation.java b/robot-core/src/main/java/org/obolibrary/robot/ExtractOperation.java index 0326e89d2..bdb0a4083 100644 --- a/robot-core/src/main/java/org/obolibrary/robot/ExtractOperation.java +++ b/robot-core/src/main/java/org/obolibrary/robot/ExtractOperation.java @@ -189,20 +189,7 @@ public static OWLOntology extract( } // Maybe annotate entities with rdfs:isDefinedBy if (OptionsHelper.optionIsTrue(options, "annotate-with-source")) { - Set sourceAxioms = new HashSet<>(); - for (OWLEntity entity : OntologyHelper.getEntities(outputOntology)) { - // Check if rdfs:isDefinedBy already exists - Set existingValues = - OntologyHelper.getAnnotationValues(outputOntology, isDefinedBy, entity.getIRI()); - if (existingValues == null || existingValues.size() == 0) { - // If not, add it - OWLAnnotationAxiom def = getIsDefinedBy(entity, sourceMap); - if (def != null) { - sourceAxioms.add(def); - } - } - } - manager.addAxioms(outputOntology, sourceAxioms); + annotateWithSource(sourceMap, outputOntology, manager); } // Determine what to do based on intermediates @@ -219,6 +206,31 @@ public static OWLOntology extract( } } + /** + * Annotates entities of the outputOntology with rdfs:isDefinedBy. + * + * @param sourceMap map of term IRI to source IRI, or null. + * @param outputOntology output ontology. + * @param manager OWL ontology manager. + */ + private static void annotateWithSource( + Map sourceMap, OWLOntology outputOntology, OWLOntologyManager manager) { + Set sourceAxioms = new HashSet<>(); + for (OWLEntity entity : OntologyHelper.getEntities(outputOntology)) { + // Check if rdfs:isDefinedBy already exists + Set existingValues = + OntologyHelper.getAnnotationValues(outputOntology, isDefinedBy, entity.getIRI()); + if (existingValues == null || existingValues.size() == 0) { + // If not, add it + OWLAnnotationAxiom def = getIsDefinedBy(entity, sourceMap); + if (def != null) { + sourceAxioms.add(def); + } + } + } + manager.addAxioms(outputOntology, sourceAxioms); + } + /** * Extracts a materialized sub-ontology from the given ontology that only contains the given terms * and the relations between them. The input ontology is not changed. @@ -249,6 +261,11 @@ public static OWLOntology extractSubset( copyPropertyAnnotations(inputOntology, filteredOnt); ReduceOperation.reduce(filteredOnt, new org.semanticweb.elk.owlapi.ElkReasonerFactory()); + // Maybe annotate entities with rdfs:isDefinedBy + if (OptionsHelper.optionIsTrue(options, "annotate-with-source")) { + annotateWithSource(sourceMap, filteredOnt, OWLManager.createOWLOntologyManager()); + } + return filteredOnt; } diff --git a/robot-core/src/main/java/org/obolibrary/robot/IOHelper.java b/robot-core/src/main/java/org/obolibrary/robot/IOHelper.java index eb2c14970..24fc21dd2 100644 --- a/robot-core/src/main/java/org/obolibrary/robot/IOHelper.java +++ b/robot-core/src/main/java/org/obolibrary/robot/IOHelper.java @@ -859,6 +859,16 @@ public OWLOntology saveOntology( saveCompressedOntology(data, ontologyIRI); return ontology; } + OWLDocumentFormat previousFormat = ontology.getOWLOntologyManager().getOntologyFormat(ontology); + if (format.isPrefixOWLOntologyFormat() + && previousFormat != null + && previousFormat.isPrefixOWLOntologyFormat()) { + String defaultNamespace = format.asPrefixOWLOntologyFormat().getDefaultPrefix(); + format + .asPrefixOWLOntologyFormat() + .copyPrefixesFrom(previousFormat.asPrefixOWLOntologyFormat()); + format.asPrefixOWLOntologyFormat().setDefaultPrefix(defaultNamespace); + } // If not compressed, just save the file as-is if (addPrefixes != null && !addPrefixes.isEmpty()) { addPrefixes(format, addPrefixes); diff --git a/robot-core/src/main/java/org/obolibrary/robot/MergeOperation.java b/robot-core/src/main/java/org/obolibrary/robot/MergeOperation.java index 92d1a13a4..6e5efa25d 100644 --- a/robot-core/src/main/java/org/obolibrary/robot/MergeOperation.java +++ b/robot-core/src/main/java/org/obolibrary/robot/MergeOperation.java @@ -363,8 +363,10 @@ private static void annotateWithOntologyIRI( OWLAnnotationProperty rdfsIsDefinedBy = targetOntology.getOWLOntologyManager().getOWLDataFactory().getRDFSIsDefinedBy(); for (OWLEntity owlEntity : sourceOntology.getSignature(includeImportsClosure)) { - OntologyHelper.addEntityAnnotation( - targetOntology, owlEntity, rdfsIsDefinedBy, ontIRI, false); + if (!owlEntity.getIRI().isReservedVocabulary()) { + OntologyHelper.addEntityAnnotation( + targetOntology, owlEntity, rdfsIsDefinedBy, ontIRI, false); + } } } } diff --git a/robot-core/src/main/java/org/obolibrary/robot/QuotedEntityChecker.java b/robot-core/src/main/java/org/obolibrary/robot/QuotedEntityChecker.java index dc81dc6f1..e5840fffe 100644 --- a/robot-core/src/main/java/org/obolibrary/robot/QuotedEntityChecker.java +++ b/robot-core/src/main/java/org/obolibrary/robot/QuotedEntityChecker.java @@ -249,6 +249,9 @@ public void add(OWLEntity entity, String name) { if (entity == null) { return; } + if (name == null) { + return; + } Map map = pickMap(entity); if (map == null) { diff --git a/robot-core/src/main/java/org/obolibrary/robot/ReasonerHelper.java b/robot-core/src/main/java/org/obolibrary/robot/ReasonerHelper.java index 773838912..9acdbcebd 100644 --- a/robot-core/src/main/java/org/obolibrary/robot/ReasonerHelper.java +++ b/robot-core/src/main/java/org/obolibrary/robot/ReasonerHelper.java @@ -13,6 +13,7 @@ import org.obolibrary.robot.reason.InferredSubObjectPropertyAxiomGeneratorIncludingIndirect; import org.semanticweb.owlapi.model.*; import org.semanticweb.owlapi.model.parameters.Imports; +import org.semanticweb.owlapi.reasoner.InferenceType; import org.semanticweb.owlapi.reasoner.OWLReasoner; import org.semanticweb.owlapi.util.*; import org.slf4j.Logger; @@ -36,6 +37,67 @@ public class ReasonerHelper { /** Logger. */ private static final Logger logger = LoggerFactory.getLogger(ReasonerHelper.class); + /** + * Check an ontology for unsatisfiable object properties. + * + * @param reasoner OWLReasoner being used. + * @return A set of unsatisfiable OWLObjectProperty objects (will be empty if none are found). + */ + public static Set getUnsatisfiableObjectProperties(OWLReasoner reasoner) { + Set unsatObjectProps = new HashSet<>(); + + if (reasoner.getPrecomputableInferenceTypes().contains(InferenceType.OBJECT_PROPERTY_HIERARCHY) + && !reasoner.getClass().getName().equals("org.semanticweb.elk.owlapi.ElkReasoner")) { + // Fast object-unsat check + logger.info( + "Object-property precomputation is supported; using that to find unsatisfiable object properties..."); + reasoner.precomputeInferences(InferenceType.OBJECT_PROPERTY_HIERARCHY); + Set unsatObjPropExps = + reasoner.getBottomObjectPropertyNode().getEntitiesMinusBottom(); + for (OWLObjectPropertyExpression uOPE : unsatObjPropExps) { + if (uOPE.isNamed()) { + unsatObjectProps.add(uOPE.asOWLObjectProperty()); + } + } + } else { + // Have to do this the slow way + OWLOntology ont = reasoner.getRootOntology(); + OWLOntologyManager manager = ont.getOWLOntologyManager(); + OWLDataFactory dataFactory = manager.getOWLDataFactory(); + OWLClass thing = dataFactory.getOWLThing(); + Set tempAxioms = new HashSet<>(); + Map probeFor = new HashMap<>(); + + for (OWLObjectProperty p : ont.getObjectPropertiesInSignature(Imports.INCLUDED)) { + UUID uuid = UUID.randomUUID(); + IRI probeIRI = IRI.create(p.getIRI().toString() + "-" + uuid.toString()); + OWLClass probe = dataFactory.getOWLClass(probeIRI); + probeFor.put(probe, p); + tempAxioms.add(dataFactory.getOWLDeclarationAxiom(probe)); + tempAxioms.add( + dataFactory.getOWLSubClassOfAxiom( + probe, dataFactory.getOWLObjectSomeValuesFrom(p, thing))); + } + manager.addAxioms(ont, tempAxioms); + reasoner.flush(); + + Set unsatisfiableProbeClasses = + reasoner.getUnsatisfiableClasses().getEntitiesMinusBottom(); + + // leave no trace + manager.removeAxioms(ont, tempAxioms); + reasoner.flush(); + + if (unsatisfiableProbeClasses.size() > 0) { + for (OWLClass cls : unsatisfiableProbeClasses) { + OWLObjectProperty unsatP = probeFor.get(cls); + unsatObjectProps.add(unsatP); + } + } + } + return unsatObjectProps; + } + /** * Validates ontology. * @@ -118,40 +180,14 @@ public static void validate( throw new IncoherentTBoxException(unsatisfiableClasses); } - // TODO: can this be done by checking for equivalence to bottomObjectProperty? logger.info("Checking for unsatisfiable object properties..."); - Set tempAxioms = new HashSet<>(); - Map probeFor = new HashMap<>(); - for (OWLObjectProperty p : ont.getObjectPropertiesInSignature(Imports.INCLUDED)) { - UUID uuid = UUID.randomUUID(); - IRI probeIRI = IRI.create(p.getIRI().toString() + "-" + uuid.toString()); - OWLClass probe = dataFactory.getOWLClass(probeIRI); - probeFor.put(probe, p); - tempAxioms.add(dataFactory.getOWLDeclarationAxiom(probe)); - tempAxioms.add( - dataFactory.getOWLSubClassOfAxiom( - probe, dataFactory.getOWLObjectSomeValuesFrom(p, thing))); - } - manager.addAxioms(ont, tempAxioms); - reasoner.flush(); - - Set unsatisfiableProbeClasses = - reasoner.getUnsatisfiableClasses().getEntitiesMinus(nothing); + Set unsatPs = getUnsatisfiableObjectProperties(reasoner); - // leave no trace - manager.removeAxioms(ont, tempAxioms); - reasoner.flush(); - - if (unsatisfiableProbeClasses.size() > 0) { - logger.error( - "There are {} unsatisfiable properties in the ontology.", - unsatisfiableProbeClasses.size()); - Set unsatPs = new HashSet<>(); - for (OWLClass cls : unsatisfiableProbeClasses) { - OWLObjectProperty unsatP = probeFor.get(cls); - unsatPs.add(unsatP); - logger.error(" unsatisfiable property: " + unsatP.getIRI()); + if (unsatPs.size() > 0) { + logger.error("There are {} unsatisfiable properties in the ontology.", unsatPs.size()); + for (OWLObjectProperty p : unsatPs) { + logger.error(" unsatisfiable property: " + p.getIRI()); } throw new IncoherentRBoxException(unsatPs); } diff --git a/robot-core/src/main/java/org/obolibrary/robot/Template.java b/robot-core/src/main/java/org/obolibrary/robot/Template.java index f98b331fa..d95ec8135 100644 --- a/robot-core/src/main/java/org/obolibrary/robot/Template.java +++ b/robot-core/src/main/java/org/obolibrary/robot/Template.java @@ -173,7 +173,7 @@ public Template(@Nonnull String name, @Nonnull List> rows) throws E // Add the contents of the tableRows addTable(rows); - addLabels(); + addEntities(); createParser(); } @@ -203,7 +203,7 @@ public Template(@Nonnull String name, @Nonnull List> rows, IOHelper // Add the contents of the tableRows addTable(rows); - addLabels(); + addEntities(); createParser(); } @@ -237,7 +237,7 @@ public Template(@Nonnull String name, @Nonnull List> rows, OWLOntol // Add the contents of the tableRows addTable(rows); - addLabels(); + addEntities(); createParser(); } @@ -276,7 +276,7 @@ public Template( // Add the contents of the tableRows addTable(rows); - addLabels(); + addEntities(); createParser(); } @@ -320,7 +320,7 @@ public Template( // Add the contents of the tableRows addTable(rows); - addLabels(); + addEntities(); createParser(); parser.setOWLEntityChecker(this.checker); } @@ -557,102 +557,103 @@ private void addTable(List> rows) throws Exception { } } - /** Add the labels from the rows of the template to the QuotedEntityChecker. */ - private void addLabels() { - // If there's no label column, we can't add labels - if (labelColumn == -1) { + /** Add the entities from the rows of the template to the QuotedEntityChecker. */ + private void addEntities() { + for (List row : tableRows) { + addEntity(row); + } + } + + /** Add the entity from this row of the template to the QuotedEntityChecker. */ + private void addEntity(List row) { + String id = null; + try { + id = row.get(idColumn); + } catch (IndexOutOfBoundsException e) { + // ignore + } + + if (id == null) { return; } - for (List row : tableRows) { - String id = null; - if (idColumn != -1) { - try { - id = row.get(idColumn); - } catch (IndexOutOfBoundsException e) { - // ignore - } - } - String label = null; + String label = null; + try { + label = row.get(labelColumn); + } catch (IndexOutOfBoundsException e) { + // ignore + } + + String type = null; + if (typeColumn != -1) { try { - label = row.get(labelColumn); + type = row.get(typeColumn); } catch (IndexOutOfBoundsException e) { // ignore } + } + if (type == null || type.trim().isEmpty()) { + type = "class"; + } - if (idColumn != -1 && id == null) { - continue; - } - - if (id == null || label == null) { - continue; - } - - String type = null; - if (typeColumn != -1) { - try { - type = row.get(typeColumn); - } catch (IndexOutOfBoundsException e) { - // ignore - } - } - if (type == null || type.trim().isEmpty()) { - type = "class"; - } + IRI iri = ioHelper.createIRI(id); + if (iri == null) { + iri = IRI.create(id); + } - IRI iri = ioHelper.createIRI(id); - if (iri == null) { - iri = IRI.create(id); - } + // Try to resolve a CURIE + IRI typeIRI = ioHelper.createIRI(type); - // Try to resolve a CURIE - IRI typeIRI = ioHelper.createIRI(type); + // Set to IRI string or to type string + String typeOrIRI = type; + if (typeIRI != null) { + typeOrIRI = typeIRI.toString(); + } - // Set to IRI string or to type string - String typeOrIRI = type; - if (typeIRI != null) { - typeOrIRI = typeIRI.toString(); - } + // Check against builtin types (ignore case), otherwise treat as individual + OWLEntity entity; + String lowerCaseType = typeOrIRI.toLowerCase(); + switch (lowerCaseType) { + case "": + case "http://www.w3.org/2002/07/owl#class": + case "class": + entity = dataFactory.getOWLEntity(EntityType.CLASS, iri); + break; - // Check against builtin types (ignore case), otherwise treat as individual - OWLEntity entity; - String lowerCaseType = typeOrIRI.toLowerCase(); - switch (lowerCaseType) { - case "": - case "http://www.w3.org/2002/07/owl#class": - case "class": - entity = dataFactory.getOWLEntity(EntityType.CLASS, iri); - break; + case "http://www.w3.org/2002/07/owl#objectproperty": + case "object property": + entity = dataFactory.getOWLEntity(EntityType.OBJECT_PROPERTY, iri); + break; - case "http://www.w3.org/2002/07/owl#objectproperty": - case "object property": - entity = dataFactory.getOWLEntity(EntityType.OBJECT_PROPERTY, iri); - break; + case "http://www.w3.org/2002/07/owl#dataproperty": + case "data property": + entity = dataFactory.getOWLEntity(EntityType.DATA_PROPERTY, iri); + break; - case "http://www.w3.org/2002/07/owl#dataproperty": - case "data property": - entity = dataFactory.getOWLEntity(EntityType.DATA_PROPERTY, iri); - break; + case "http://www.w3.org/2002/07/owl#annotationproperty": + case "annotation property": + entity = dataFactory.getOWLEntity(EntityType.ANNOTATION_PROPERTY, iri); + break; - case "http://www.w3.org/2002/07/owl#annotationproperty": - case "annotation property": - entity = dataFactory.getOWLEntity(EntityType.ANNOTATION_PROPERTY, iri); - break; + case "http://www.w3.org/2002/07/owl#datatype": + case "datatype": + entity = dataFactory.getOWLEntity(EntityType.DATATYPE, iri); + break; - case "http://www.w3.org/2002/07/owl#datatype": - case "datatype": - entity = dataFactory.getOWLEntity(EntityType.DATATYPE, iri); - break; + case "http://www.w3.org/2002/07/owl#individual": + case "individual": + case "http://www.w3.org/2002/07/owl#namedindividual": + case "named individual": + default: + // Assume type is an individual (checked later) + entity = dataFactory.getOWLEntity(EntityType.NAMED_INDIVIDUAL, iri); + break; + } - case "http://www.w3.org/2002/07/owl#individual": - case "individual": - case "http://www.w3.org/2002/07/owl#namedindividual": - case "named individual": - default: - // Assume type is an individual (checked later) - entity = dataFactory.getOWLEntity(EntityType.NAMED_INDIVIDUAL, iri); - break; - } + if (id != null) { + checker.add(entity, id); + } + if (label != null) { checker.add(entity, label); } } @@ -795,6 +796,8 @@ private void processRow(List row) throws Exception { case "http://www.w3.org/2002/07/owl#namedindividual": case "named individual": default: + // This is a bit unsafe imo, for example in the case of where the datatype turns out to be + // http://www.w3.org/2002/07/owl#DatatypeProperty"" addIndividualAxioms(iri, row); break; } diff --git a/robot-core/src/main/java/org/obolibrary/robot/export/Row.java b/robot-core/src/main/java/org/obolibrary/robot/export/Row.java index 0bd7aa33d..60b65fedf 100644 --- a/robot-core/src/main/java/org/obolibrary/robot/export/Row.java +++ b/robot-core/src/main/java/org/obolibrary/robot/export/Row.java @@ -82,8 +82,8 @@ public void addToWorkbook(Workbook wb, List columns, String split) { String value; String comment = null; - CellStyle style = wb.createCellStyle(); - Font font = wb.createFont(); + CellStyle style = null; + Font font = null; if (cell != null) { List values = cell.getDisplayValues(); if (values.size() > 1) { @@ -109,6 +109,9 @@ public void addToWorkbook(Workbook wb, List columns, String split) { } if (cellColor != null) { + if (style == null) { + style = wb.createCellStyle(); + } style.setFillForegroundColor(cellColor.getIndex()); } @@ -116,13 +119,24 @@ public void addToWorkbook(Workbook wb, List columns, String split) { if (cellColor != null && cellPattern == null) { cellPattern = FillPatternType.SOLID_FOREGROUND; } + if (cellPattern != null) { + if (style == null) { + style = wb.createCellStyle(); + } style.setFillPattern(cellPattern); } IndexedColors fontColor = cell.getFontColor(); if (fontColor != null) { + if (style == null) { + style = wb.createCellStyle(); + } + if (font == null) { + font = wb.createFont(); + } font.setColor(fontColor.getIndex()); + style.setFont(font); } // Maybe get a comment or null @@ -135,8 +149,7 @@ public void addToWorkbook(Workbook wb, List columns, String split) { // Add value to cell xlsxCell.setCellValue(value); - // Add style to cell - style.setFont(font); + // Add style to cell, null is OK xlsxCell.setCellStyle(style); // Maybe add a comment diff --git a/robot-core/src/main/resources/obo_context.jsonld b/robot-core/src/main/resources/obo_context.jsonld index 488a1d2be..b52c0d408 100644 --- a/robot-core/src/main/resources/obo_context.jsonld +++ b/robot-core/src/main/resources/obo_context.jsonld @@ -138,6 +138,7 @@ "MAT": "http://purl.obolibrary.org/obo/MAT_", "MAXO": "http://purl.obolibrary.org/obo/MAXO_", "MCO": "http://purl.obolibrary.org/obo/MCO_", + "MCRO": "http://purl.obolibrary.org/obo/MCRO_", "MF": "http://purl.obolibrary.org/obo/MF_", "MFMO": "http://purl.obolibrary.org/obo/MFMO_", "MFO": "http://purl.obolibrary.org/obo/MFO_", diff --git a/robot-core/src/main/resources/report_queries/multiple_definitions.rq b/robot-core/src/main/resources/report_queries/multiple_definitions.rq index 48ea17760..b394d4843 100644 --- a/robot-core/src/main/resources/report_queries/multiple_definitions.rq +++ b/robot-core/src/main/resources/report_queries/multiple_definitions.rq @@ -1,4 +1,4 @@ -# # Multiple Defintions +# # Multiple Definitions # # **Problem:** An entity has more than one definition or elucidation. This may cause confusion or misuse, and will prevent translation to OBO format (in case of multiple definitions). Excludes deprecated entities. # diff --git a/robot-core/src/test/java/org/obolibrary/robot/DiffOperationTest.java b/robot-core/src/test/java/org/obolibrary/robot/DiffOperationTest.java index 0e3b8bf73..bba25629b 100644 --- a/robot-core/src/test/java/org/obolibrary/robot/DiffOperationTest.java +++ b/robot-core/src/test/java/org/obolibrary/robot/DiffOperationTest.java @@ -55,7 +55,8 @@ public void testCompareModified() throws IOException, OWLOntologyCreationExcepti assertFalse(actual); String expected = IOUtils.toString( - this.getClass().getResourceAsStream("/simple1.diff"), Charset.defaultCharset()); + this.getClass().getResourceAsStream("/simple1.diff"), Charset.defaultCharset()) + .replaceAll("\r\n", "\n"); assertEquals(expected, writer.toString()); } @@ -77,7 +78,8 @@ public void testCompareModifiedWithLabels() throws IOException { assertFalse(actual); String expected = IOUtils.toString( - this.getClass().getResourceAsStream("/simple.diff"), Charset.defaultCharset()); + this.getClass().getResourceAsStream("/simple.diff"), Charset.defaultCharset()) + .replaceAll("\r\n", "\n"); assertEquals(expected, writer.toString()); } diff --git a/robot-core/src/test/java/org/obolibrary/robot/ExportOperationTest.java b/robot-core/src/test/java/org/obolibrary/robot/ExportOperationTest.java index b394e458e..f33b45d62 100644 --- a/robot-core/src/test/java/org/obolibrary/robot/ExportOperationTest.java +++ b/robot-core/src/test/java/org/obolibrary/robot/ExportOperationTest.java @@ -1,14 +1,29 @@ package org.obolibrary.robot; +import static org.junit.Assert.assertEquals; + +import java.io.FileOutputStream; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Set; +import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.junit.Test; import org.obolibrary.robot.export.Table; +import org.semanticweb.owlapi.apibinding.OWLManager; +import org.semanticweb.owlapi.model.AxiomType; +import org.semanticweb.owlapi.model.OWLClass; +import org.semanticweb.owlapi.model.OWLEntity; +import org.semanticweb.owlapi.model.OWLObjectProperty; import org.semanticweb.owlapi.model.OWLOntology; +import org.semanticweb.owlapi.model.OWLOntologyManager; +import org.semanticweb.owlapi.model.OWLSubClassOfAxiom; +import org.semanticweb.owlapi.model.parameters.Imports; +import org.semanticweb.owlapi.util.DefaultPrefixManager; /** * Tests ExportOperation. @@ -17,6 +32,23 @@ */ public class ExportOperationTest extends CoreTest { + /** + * Utility method to write XLSX file for debugging. + * + * @param workbook Workfbook to write + * @param path Path (string) to save to, under `robot-core/` + */ + void writeXLSX(Workbook workbook, String path) { + try { + System.out.println("Writing result to robot-core/" + path); + FileOutputStream out = new FileOutputStream(path); + workbook.write(out); + out.close(); + } catch (Exception ex) { + System.out.println("Error writing workbook: " + ex); + } + } + /** * Test exporting simple.owl to XLSX. * @@ -53,4 +85,201 @@ public void testExportToXLSX() throws Exception { v2 = r2.getCell(1).getStringCellValue(); assert (v2.equals("simple:test1")); } + + /** + * Test exporting to Excel with more than 64,000 exported records. + * + * @throws Exception on any problem + */ + @Test + public void testLargeExcelExport() throws Exception { + + // how many many axioms to create + int axiomCount = 64002; + + IOHelper ioHelper = new IOHelper(); + ioHelper.addPrefix( + "simple", "https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#"); + + DefaultPrefixManager pfm = ioHelper.getPrefixManager(); + + OWLOntologyManager owlm = OWLManager.createOWLOntologyManager(); + OWLOntology ontology = owlm.createOntology(); + + // add axiomCount subclass axioms to ontology + // first the super class + OWLClass superClass = CoreTest.dataFactory.getOWLClass("simple:super", pfm); + + // then all the subs and axioms + for (int i = 0; i < axiomCount; i++) { + OWLClass subClass = CoreTest.dataFactory.getOWLClass("simple:sub_" + i, pfm); + OWLSubClassOfAxiom subAxiom = + CoreTest.dataFactory.getOWLSubClassOfAxiom(subClass, superClass); + owlm.addAxiom(ontology, subAxiom); + } + + // export just the classes and their subclasses + List columns = Arrays.asList("ID", "SubClass Of [ID]"); + Map options = ExportOperation.getDefaultOptions(); + options.put("include", "classes"); + + // Create the table and render it as a Workbook + Table t = ExportOperation.createExportTable(ontology, ioHelper, columns, options); + Workbook wb = t.asWorkbook("|"); + + // get the first sheet + Sheet s = wb.getSheetAt(0); + + // There should be same number of rows as subclass axioms + 1 for the header row + assert (s.getLastRowNum() == ontology.getAxiomCount(AxiomType.SUBCLASS_OF) + 1); + } + + /** + * Test exporting all named headings using simple ontology. + * + * @throws Exception on any problem + */ + @Test + public void testExportNamedHeadingsSimple() throws Exception { + OWLOntology ontology = loadOntology("/simple_defined_by.owl"); + IOHelper ioHelper = new IOHelper(); + ioHelper.addPrefix( + "simple", "https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#"); + + // every named header + List columns = + Arrays.asList( + "ID", + "LABEL", + "IRI", + "CURIE", + "Type", + "SYNONYMS", + "rdfs:isDefinedBy", + "SUBCLASSES", + "SubClass Of", + "SubProperty Of", + "Equivalent Class", + "Equivalent Property", + "Disjoint With", + "Domain", + "Range"); + + // export everything + Map options = ExportOperation.getDefaultOptions(); + options.put("include", "classes individuals properties"); + + // Create the table and render it as a Workbook + Table t = ExportOperation.createExportTable(ontology, ioHelper, columns, options); + Workbook wb = t.asWorkbook("|"); + + Sheet s = wb.getSheetAt(0); + assertEquals(s.getLastRowNum(), 4); + + // Validate header + // should match size and label + org.apache.poi.ss.usermodel.Row header = s.getRow(0); + + short minColIx = header.getFirstCellNum(); + short maxColIx = header.getLastCellNum(); + int numCol = maxColIx - minColIx; + // same width + assert (numCol == columns.size()); + + // column header labels should match + for (short colIx = minColIx; colIx < maxColIx; colIx++) { + Cell cell = header.getCell(colIx); + if (cell == null) { + continue; + } + String foundHeader = cell.getStringCellValue(); + String expectedHeader = columns.get(colIx); + assert (foundHeader.equals(expectedHeader)); + } + + // rdfs:isDefinedBy should work + assertEquals( + s.getRow(4).getCell(6).getStringCellValue(), + "https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl"); + } + + /** + * Test exporting all named headings and object properties seen in the all-axioms ontology. + * + * @throws Exception on any problem + */ + @Test + public void testExportNamedHeadingsAllAxioms() throws Exception { + OWLOntology ontology = loadOntology("/axioms.owl"); + Set signature = ontology.getSignature(); + // remove datatypes + signature.removeAll(ontology.getDatatypesInSignature()); + int entityCount = signature.size(); + + Set properties = ontology.getObjectPropertiesInSignature(Imports.EXCLUDED); + + IOHelper ioHelper = new IOHelper(); + ioHelper.addPrefix("ax", "https://http://robot.obolibrary.org/export_test/"); + + // every named header + List columns = new ArrayList(); + + List hdrLabels = + Arrays.asList( + "ID", + "LABEL", + "Type", + "IRI", + "CURIE", + "SYNONYMS", + "SUBCLASSES", + "SubClass Of", + "SubProperty Of", + "Equivalent Class", + "Equivalent Property", + "Disjoint With", + "Domain", + "Range"); + + columns.addAll(hdrLabels); + + // add all the object properties to the header + for (OWLObjectProperty op : properties) { + String shrt = ioHelper.getPrefixManager().getShortForm(op.getIRI()); + columns.add(shrt); + } + + // export everything + Map options = ExportOperation.getDefaultOptions(); + options.put("include", "classes individuals properties"); + + // Create the table and render it as a Workbook + Table t = ExportOperation.createExportTable(ontology, ioHelper, columns, options); + Workbook wb = t.asWorkbook("|"); + + Sheet s = wb.getSheetAt(0); + // There should be same number of rows as class,properties and instances in signature + assert (s.getLastRowNum() == entityCount); + + // Validate header + // should match size and label + org.apache.poi.ss.usermodel.Row header = s.getRow(0); + + short minColIx = header.getFirstCellNum(); + short maxColIx = header.getLastCellNum(); + int numCol = maxColIx - minColIx; + // should be same width + assert (numCol == columns.size()); + + // column header labels should match workbook + for (short colIx = minColIx; colIx < maxColIx; colIx++) { + Cell cell = header.getCell(colIx); + if (cell == null) { + continue; + } + String foundHeader = cell.getStringCellValue(); + String expectedHeader = columns.get(colIx); + assert (foundHeader.equals(expectedHeader)); + } + } } diff --git a/robot-core/src/test/java/org/obolibrary/robot/IOHelperTest.java b/robot-core/src/test/java/org/obolibrary/robot/IOHelperTest.java index 4324753f4..8ecf95131 100644 --- a/robot-core/src/test/java/org/obolibrary/robot/IOHelperTest.java +++ b/robot-core/src/test/java/org/obolibrary/robot/IOHelperTest.java @@ -53,7 +53,7 @@ public void testAddPrefixes() throws IOException { ioh.addPrefixes(context); // Get the context back from IOHelper - String outputContext = ioh.getContextString(); + String outputContext = ioh.getContextString().replaceAll("\r\n", "\n"); assertEquals(inputContext, outputContext); } @@ -166,7 +166,7 @@ public void testPrefixHandling() throws IOException { String json = "{\n" + " \"@context\" : {\n" + " \"foo\" : \"http://example.com#\"\n" + " }\n" + "}"; - assertEquals("Check JSON-LD", json, ioh.getContextString()); + assertEquals("Check JSON-LD", json, ioh.getContextString().replaceAll("\r\n", "\n")); ioh.addPrefix("bar: http://example.com#"); expected.put("bar", "http://example.com#"); @@ -189,6 +189,37 @@ public void testPrefixManager() throws IOException { pm.getIRI("GO:12345").toString()); } + /** + * Test that original prefixes are preserved when saving. + * + * @throws IOException on file problem + */ + @Test + public void testPrefixConservation() throws IOException { + OWLOntology ontology = loadOntology("/simple.owl"); + String origNamespace = + ontology + .getOWLOntologyManager() + .getOntologyFormat(ontology) + .asPrefixOWLOntologyFormat() + .getPrefix("obo:"); + + File tempFile = File.createTempFile("simple-roundtrip", ".owl"); + tempFile.deleteOnExit(); + IOHelper ioHelper = new IOHelper(); + ioHelper.saveOntology(ontology, new RDFXMLDocumentFormat(), tempFile); + + OWLOntology ontology2 = ioHelper.loadOntology(tempFile.getPath()); + String savedNamespace = + ontology2 + .getOWLOntologyManager() + .getOntologyFormat(ontology2) + .asPrefixOWLOntologyFormat() + .getPrefix("obo:"); + + assertEquals(origNamespace, savedNamespace); + } + /** * Test getting terms from strings. * diff --git a/robot-core/src/test/java/org/obolibrary/robot/MergeOperationTest.java b/robot-core/src/test/java/org/obolibrary/robot/MergeOperationTest.java index 6bf104b2f..386a4319c 100644 --- a/robot-core/src/test/java/org/obolibrary/robot/MergeOperationTest.java +++ b/robot-core/src/test/java/org/obolibrary/robot/MergeOperationTest.java @@ -101,9 +101,9 @@ public void testMergeDefinedBy() throws IOException { assertEquals(5, simple.getAxiomCount()); OWLOntology merged = MergeOperation.merge(ontologies, false, false, true, false); - assertEquals(9, merged.getAxiomCount()); + assertEquals(7, merged.getAxiomCount()); OWLOntology expected = loadOntology("/simple_defined_by.owl"); - assertEquals(9, expected.getAxiomCount()); + assertEquals(7, expected.getAxiomCount()); assertIdentical(expected, merged); } } diff --git a/robot-core/src/test/java/org/obolibrary/robot/ReasonerHelperTest.java b/robot-core/src/test/java/org/obolibrary/robot/ReasonerHelperTest.java index df6834404..aaee02de0 100644 --- a/robot-core/src/test/java/org/obolibrary/robot/ReasonerHelperTest.java +++ b/robot-core/src/test/java/org/obolibrary/robot/ReasonerHelperTest.java @@ -3,6 +3,8 @@ import static org.junit.Assert.*; import java.io.IOException; +import java.util.Arrays; +import java.util.List; import java.util.Set; import org.junit.Test; import org.obolibrary.robot.exceptions.IncoherentRBoxException; @@ -34,16 +36,21 @@ public class ReasonerHelperTest extends CoreTest { public void testIncoherentRBox() throws IOException, IncoherentTBoxException, InconsistentOntologyException { OWLOntology ontology = loadOntology("/incoherent-rbox.owl"); - OWLReasonerFactory reasonerFactory = new org.semanticweb.elk.owlapi.ElkReasonerFactory(); - OWLReasoner reasoner = reasonerFactory.createReasoner(ontology); - boolean isCaughtException = false; - try { - ReasonerHelper.validate(reasoner); + List factories = + Arrays.asList( + new org.semanticweb.elk.owlapi.ElkReasonerFactory(), + new org.semanticweb.HermiT.ReasonerFactory()); + for (OWLReasonerFactory reasonerFactory : factories) { + OWLReasoner reasoner = reasonerFactory.createReasoner(ontology); + boolean isCaughtException = false; + try { + ReasonerHelper.validate(reasoner); - } catch (IncoherentRBoxException e) { - isCaughtException = true; + } catch (IncoherentRBoxException e) { + isCaughtException = true; + } + assertTrue(isCaughtException); } - assertTrue(isCaughtException); } /** @@ -57,16 +64,21 @@ public void testIncoherentRBox() public void testIncoherentTBox() throws IOException, InconsistentOntologyException, IncoherentRBoxException { OWLOntology ontology = loadOntology("/incoherent-tbox.owl"); - OWLReasonerFactory reasonerFactory = new org.semanticweb.elk.owlapi.ElkReasonerFactory(); - OWLReasoner reasoner = reasonerFactory.createReasoner(ontology); - boolean isCaughtException = false; - try { - ReasonerHelper.validate(reasoner); + List factories = + Arrays.asList( + new org.semanticweb.elk.owlapi.ElkReasonerFactory(), + new org.semanticweb.HermiT.ReasonerFactory()); + for (OWLReasonerFactory reasonerFactory : factories) { + OWLReasoner reasoner = reasonerFactory.createReasoner(ontology); + boolean isCaughtException = false; + try { + ReasonerHelper.validate(reasoner); - } catch (IncoherentTBoxException e) { - isCaughtException = true; + } catch (IncoherentTBoxException e) { + isCaughtException = true; + } + assertTrue(isCaughtException); } - assertTrue(isCaughtException); } /** diff --git a/robot-core/src/test/java/org/obolibrary/robot/ReportOperationTest.java b/robot-core/src/test/java/org/obolibrary/robot/ReportOperationTest.java index 0f6d9a760..50803c7b0 100644 --- a/robot-core/src/test/java/org/obolibrary/robot/ReportOperationTest.java +++ b/robot-core/src/test/java/org/obolibrary/robot/ReportOperationTest.java @@ -48,12 +48,14 @@ private void testReportProducesCorrectOutput(String extension) throws Exception File.createTempFile("1016-report-json-failure-output", "." + extension); ReportOperation.report(ontology, iohelper, outputFile.toString(), Collections.emptyMap()); final String output = - IOUtils.toString(new FileInputStream(outputFile), Charset.defaultCharset()).trim(); + IOUtils.toString(new FileInputStream(outputFile), Charset.defaultCharset()) + .trim() + .replaceAll("\r\n", "\n"); final InputStream expected = getClass().getResourceAsStream("/1016-report-json-failure/output." + extension); assert expected != null; final String expectedOutput = - IOUtils.toString(expected, StandardCharsets.UTF_8.name()).trim(); + IOUtils.toString(expected, StandardCharsets.UTF_8.name()).trim().replaceAll("\r\n", "\n"); Assert.assertEquals(expectedOutput, output); } catch (YAMLException e) { e.printStackTrace(); diff --git a/robot-core/src/test/java/org/obolibrary/robot/TemplateTest.java b/robot-core/src/test/java/org/obolibrary/robot/TemplateTest.java index 8e39eca03..f283ea790 100644 --- a/robot-core/src/test/java/org/obolibrary/robot/TemplateTest.java +++ b/robot-core/src/test/java/org/obolibrary/robot/TemplateTest.java @@ -49,6 +49,35 @@ public void testLegacyTemplateCSV() throws Exception { assertIdentical("/template.owl", template); } + /** + * Test a strange case where a sequence . + * + * @throws Exception if entities cannot be found + */ + @Test + public void testNoLabelsNoTypes() throws Exception { + String path = "/sequence-template.csv"; + List> rows = TemplateHelper.readCSV(this.getClass().getResourceAsStream(path)); + Template t = new Template(path, rows); + t.generateOutputOntology("http://test.com/template.owl", false, null); + } + + /** + * Test a strange case where a sequence . + * + * @throws Exception if entities cannot be found + */ + @Test + public void testNoLabels() throws Exception { + String path = "/workflow-template.csv"; + List> rows = TemplateHelper.readCSV(this.getClass().getResourceAsStream(path)); + IOHelper ioHelper = new IOHelper(); + ioHelper.addPrefix("ex", "http://example.com/"); + Template t = new Template(path, rows, ioHelper); + OWLOntology template = t.generateOutputOntology("http://test.com/template.owl", false, null); + assertIdentical("/workflow-template.ttl", template); + } + /** * Test multiple templates. * diff --git a/robot-core/src/test/resources/axioms.owl b/robot-core/src/test/resources/axioms.owl new file mode 100644 index 000000000..549a40fdf --- /dev/null +++ b/robot-core/src/test/resources/axioms.owl @@ -0,0 +1,749 @@ + + + + Test ontology containing: + - one of all OWL Axioms + - one of all class expressions in a Subclass axiom + - 1 GCI + - 1 Class-Individual pun + - 1 Class-Object property pun + - 1 Class-Data property pun + -1 Class-Annotation property pun + + + + + + + + + + + + + AnnotationProperty + + + + + + + + + + + + + + + + + + + + + + ObjectProperty1 + + + + + + + + + + ObjectProperty2 + + + + + + + + + FunctionalObjectProperty + + + + + + + + + InverseObjectProperty + + + + + + + + + DisjointObjectProperty + + + + + + + + DuplicateObjectProperty + + + + + + + + + + + + ChainedObjectProperty + + + + + + + + OtherInverseProperty + + + + + + + + OtherDisjointObjectProperty + + + + + + + + + InverseFunctionalObjectProperty + + + + + + + + + TransitiveObjectProperty + + + + + + + + + SymmetricObjectProperty + + + + + + + + + AsymmetricObjectProperty + + + + + + + + + ReflexiveObjectProperty + + + + + + + + + IrreflexiveObjectProperty + + + + + + + + + SubObjectProperty + + + + + + + + + + + + + + + + + + + + + DataProperty1 + + + + + + + + + + DataProperty2 + + + + + + + + DuplicateDataProperty + + + + + + + + DisjointDataProperty + + + + + + + + + FunctionalDataProperty + + + + + + + + + SubDataProperty + + + + + + + + + + + + + + + + + + + + Sub + + + + + + + + + + + + + + + Equivalent1 + + + + + + + + Equivalent2 + + + + + + + + + Disjoint1 + + + + + + + + Disjoint2 + + + + + + + + + + + + DisjointUnion1 + + + + + + + + DisjointUnion2 + + + + + + + + DisjointUnion3 + + + + + + + + ClassAssertionClass + + + + + + + + GCI1 + + + + + + + + GCI2 + + + + + + + + GCI3 + + + + + + + + GCI4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + 2 + + + + + + + 2 + + + + + + + true + + + + + + + + + + + + + + + + + + 34 + + + + + + 2 + + + + + + + 2 + + + + + + + 2 + + + + ClassExpressionClass + + + + + + + + + + + + + + + + + + + + + + + ClassWithKey + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ClassAssertionInstance + + + + + + + + 99 + DatapropertyAssertionInstance + + + + + + 42 + + + + + + + + + ObjectPropertyAssertionSubject + + + + + + + + ObjectPropertyAssertionObject + + + + + + + + ObjectPropertyNegativeAssertionObject + + + + + + + + DuplicateInstance + + + + + + + + + + + + + + ObjectPropertyNegativeAssertionSubject + + + + + + + + + + + + + + Super + + + PunnedClass-Class-Instance + PunnedInstance-Class-Instance + + + PunnedClass-Class-ObjectProperty + PunnedObjectProperty-Class-ObjectProprty + + + PunnedClass-Class-DataProperty + PunnedDataPoperty-Class-DataProperty + + + PunnedAnnotationProperty-Class-AnnotationProperty + PunnedClass-Class-AnnotationProperty + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/robot-core/src/test/resources/sequence-template.csv b/robot-core/src/test/resources/sequence-template.csv new file mode 100644 index 000000000..d979b6ddb --- /dev/null +++ b/robot-core/src/test/resources/sequence-template.csv @@ -0,0 +1,4 @@ +ID,isa +ID,SC % +CL:4030028,CL:0000561 +CL:0000561,CL:0000099 diff --git a/robot-core/src/test/resources/simple_defined_by.owl b/robot-core/src/test/resources/simple_defined_by.owl index 890c1075d..06c23107a 100644 --- a/robot-core/src/test/resources/simple_defined_by.owl +++ b/robot-core/src/test/resources/simple_defined_by.owl @@ -8,43 +8,6 @@ xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"> - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/robot-core/src/test/resources/subset_result.owl b/robot-core/src/test/resources/subset_result.owl index 480b49d76..1d49b022b 100644 --- a/robot-core/src/test/resources/subset_result.owl +++ b/robot-core/src/test/resources/subset_result.owl @@ -25,21 +25,22 @@ - database_cross_reference + database_cross_reference - - + + id + - in_subset + in_subset @@ -47,7 +48,7 @@ - shorthand + shorthand @@ -67,9 +68,9 @@ - BFO:0000050 - part_of - part_of + BFO:0000050 + part_of + part_of @@ -88,7 +89,7 @@ - ONT:1 + ONT:1 @@ -103,7 +104,7 @@ - ONT:5 + ONT:5 diff --git a/robot-core/src/test/resources/workflow-template.csv b/robot-core/src/test/resources/workflow-template.csv new file mode 100644 index 000000000..f636bf563 --- /dev/null +++ b/robot-core/src/test/resources/workflow-template.csv @@ -0,0 +1,5 @@ +Identifier,Type,Class Restriction Lower,Class Restriction Upper,Domain,Range +ID,TYPE,SC %,SC %,DOMAIN,RANGE +ex:consists_of,owl:ObjectProperty,,,ex:Workflow,ex:Task +ex:Workflow,class,(ex:consists_of min 1 ex:Task),(ex:consists_of max 7 ex:Task),, +ex:Task,class,,,, diff --git a/robot-core/src/test/resources/workflow-template.ttl b/robot-core/src/test/resources/workflow-template.ttl new file mode 100644 index 000000000..3ab680dba --- /dev/null +++ b/robot-core/src/test/resources/workflow-template.ttl @@ -0,0 +1,42 @@ +@prefix owl: . +@prefix rdf: . +@prefix xml: . +@prefix xsd: . +@prefix rdfs: . +@base . + + rdf:type owl:Ontology . + +################################################################# +# Object Properties +################################################################# + +### http://example.com/consists_of + rdf:type owl:ObjectProperty ; + rdfs:domain ; + rdfs:range . + + +################################################################# +# Classes +################################################################# + +### http://example.com/Task + rdf:type owl:Class . + + +### http://example.com/Workflow + rdf:type owl:Class ; + rdfs:subClassOf [ rdf:type owl:Restriction ; + owl:onProperty ; + owl:minQualifiedCardinality "1"^^xsd:nonNegativeInteger ; + owl:onClass + ] , + [ rdf:type owl:Restriction ; + owl:onProperty ; + owl:maxQualifiedCardinality "7"^^xsd:nonNegativeInteger ; + owl:onClass + ] . + + +### Generated by the OWL API (version 4.5.26) https://github.com/owlcs/owlapi diff --git a/robot-mock-plugin/pom.xml b/robot-mock-plugin/pom.xml new file mode 100644 index 000000000..7c367c8da --- /dev/null +++ b/robot-mock-plugin/pom.xml @@ -0,0 +1,53 @@ + + 4.0.0 + + + org.obolibrary.robot + robot + 1.10.0-SNAPSHOT + + robot-mock-plugin + robot-mock-plugin + A dummy ROBOT plugin for testing and demonstration purposes. + + + + + + com.coveo + fmt-maven-plugin + 2.9 + + + + format + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.0.0-M5 + + + + integration-test + verify + + + + + + + + + + org.obolibrary.robot + robot-command + ${project.parent.version} + + + diff --git a/robot-mock-plugin/src/main/java/org/obolibrary/robot/plugins/HelloCommand.java b/robot-mock-plugin/src/main/java/org/obolibrary/robot/plugins/HelloCommand.java new file mode 100644 index 000000000..dcc89ecdd --- /dev/null +++ b/robot-mock-plugin/src/main/java/org/obolibrary/robot/plugins/HelloCommand.java @@ -0,0 +1,82 @@ +package org.obolibrary.robot.plugins; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Options; +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.AddOntologyAnnotation; +import org.semanticweb.owlapi.model.OWLAnnotation; +import org.semanticweb.owlapi.model.OWLDataFactory; +import org.semanticweb.owlapi.model.OWLOntology; +import org.semanticweb.owlapi.vocab.OWLRDFVocabulary; + +/** A dummy pluggable command for testing and demonstration purposes. */ +public class HelloCommand implements Command { + + private Options options; + + public HelloCommand() { + options = CommandLineHelper.getCommonOptions(); + options.addOption("i", "input", true, "load ontology from a file"); + options.addOption("I", "input-iri", true, "load ontology from an IRI"); + options.addOption("o", "output", true, "save ontology to a file"); + options.addOption("r", "recipient", true, "set the recipient of the hello message"); + } + + @Override + public String getName() { + return "hello"; + } + + @Override + public String getDescription() { + return "inject a hello annotation into the ontology"; + } + + @Override + public String getUsage() { + return "robot hello -r "; + } + + @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); + + String recipient = line.getOptionValue("recipient", "World"); + + OWLOntology ontology = state.getOntology(); + OWLDataFactory factory = ontology.getOWLOntologyManager().getOWLDataFactory(); + + OWLAnnotation annot = + factory.getOWLAnnotation( + factory.getOWLAnnotationProperty(OWLRDFVocabulary.RDFS_COMMENT.getIRI()), + factory.getOWLLiteral(String.format("Hello, %s", recipient))); + ontology.getOWLOntologyManager().applyChange(new AddOntologyAnnotation(ontology, annot)); + + CommandLineHelper.maybeSaveOutput(line, state.getOntology()); + + return state; + } +} diff --git a/robot-mock-plugin/src/main/resources/META-INF/services/org.obolibrary.robot.Command b/robot-mock-plugin/src/main/resources/META-INF/services/org.obolibrary.robot.Command new file mode 100644 index 000000000..c00023f24 --- /dev/null +++ b/robot-mock-plugin/src/main/resources/META-INF/services/org.obolibrary.robot.Command @@ -0,0 +1 @@ +org.obolibrary.robot.plugins.HelloCommand diff --git a/robot-mock-plugin/src/test/java/org/obolibrary/robot/PluginManagerIT.java b/robot-mock-plugin/src/test/java/org/obolibrary/robot/PluginManagerIT.java new file mode 100644 index 000000000..7d0d4db27 --- /dev/null +++ b/robot-mock-plugin/src/test/java/org/obolibrary/robot/PluginManagerIT.java @@ -0,0 +1,70 @@ +package org.obolibrary.robot; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import org.junit.Test; + +/** Tests for pluggable commands feature. */ +public class PluginManagerIT { + + /** Path to the executable script. */ + private String execPath = "../bin/robot"; + + /** Path to the build directory of the mock plugin. */ + private String pluginsPath = "target"; + + /** + * Search for a motif within a stream. + * + * @param stream the stream to search through + * @param needle the motif to search for + * @return {@code true} if the motif was found, otherwise {@code false} + * @throws IOException if any I/O error occurs when reading from the stream + */ + private boolean searchStream(InputStream stream, String needle) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); + + String line; + boolean found = false; + while ((line = reader.readLine()) != null) { + if (line.contains(needle)) { + found = true; + } + } + reader.close(); + + return found; + } + + /** + * Run a commands and check that its standard output contains a given motif. + * + * @param program the command to run + * @param needle the motif to search for + * @throws Exception if the motif is not found in the command's output + */ + private void runCommand(String program, String needle) throws Exception { + ProcessBuilder pb = new ProcessBuilder(program); + pb.environment().put("ROBOT_PLUGINS_DIRECTORY", pluginsPath); + + Process process = pb.start(); + boolean found = searchStream(process.getInputStream(), needle); + process.waitFor(); + + if (!found) { + throw new Exception("Pluggable command not available"); + } + } + + /** + * Test that the plugin manager finds the pluggable command in the mock plugin. + * + * @throws Exception on any problem + */ + @Test + public void testExecute() throws Exception { + runCommand(execPath, ":hello"); + } +} diff --git a/util/release.sh b/util/release.sh index c024e1e44..d229aae5f 100755 --- a/util/release.sh +++ b/util/release.sh @@ -146,7 +146,10 @@ git commit --all --message "Bump version to ${NEXT}" git push step "Send announcement email" -EMAIL="From: james@overton.ca +EMAIL="release--email.txt" + +cat << EOF > "${EMAIL}" +From: james@overton.ca To: obo-discuss@googlegroups.com, obo-tools@googlegroups.com Subject: ROBOT ${VERSION} Released @@ -158,16 +161,16 @@ ROBOT is a command-line tool for automating ontology development tasks. This rel See our homepage for more information about ROBOT: http://robot.obolibrary.org -James" -if [ "$(command -v draft-email)" ]; then - [ -z "${IMAP_PASSWORD-}" ] || read -rsp "Enter IMAP password:" IMAP_PASSWORD - echo "${EMAIL}" | draft-email - echo "A draft email has been created" +James +EOF + +if command -v himalaya > /dev/null +then + himalaya --mailbox Drafts save < "${EMAIL}" + echo "Draft email created" else - echo "Please send an email like this:" - echo "${EMAIL}" + echo "Draft email created in ${EMAIL}" fi -confirm "Sent?" trap '' INT TERM EXIT echo "Draft GitHub release created for ROBOT ${VERSION}"