/* * Licensed under the Apache License, Version 2.0 (the "License"); * * You may not use this file except in compliance with the License. * * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * * See the License for the specific language governing permissions and * limitations under the License. * * Contributions from 2013-2017 where performed either by US government * employees, or under US Veterans Health Administration contracts. * * US Veterans Health Administration contributions by government employees * are work of the U.S. Government and are not subject to copyright * protection in the United States. Portions contributed by government * employees are USGovWork (17USC ยง105). Not subject to copyright. * * Contribution by contractors to the US Veterans Health Administration * during this period are contractually contributed under the * Apache License, Version 2.0. * * See: https://www.usa.gov/government-works * * Contributions prior to 2013: * * Copyright (C) International Health Terminology Standards Development Organisation. * Licensed under the Apache License, Version 2.0. * */ package sh.isaac.api; //~--- JDK imports ------------------------------------------------------------ import java.io.DataOutputStream; import java.io.IOException; import java.io.Writer; import java.nio.file.Path; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Optional; import java.util.Stack; import java.util.TreeMap; import java.util.UUID; //~--- non-JDK imports -------------------------------------------------------- import org.jvnet.hk2.annotations.Contract; import sh.isaac.api.bootstrap.TermAux; import sh.isaac.api.commit.CommitService; import sh.isaac.api.component.concept.ConceptBuilder; import sh.isaac.api.component.concept.ConceptChronology; import sh.isaac.api.component.concept.ConceptService; import sh.isaac.api.component.concept.ConceptSpecification; import sh.isaac.api.component.concept.ConceptVersion; import sh.isaac.api.component.concept.description.DescriptionBuilder; import sh.isaac.api.component.concept.description.DescriptionBuilderService; import sh.isaac.api.component.sememe.SememeBuilder; import sh.isaac.api.component.sememe.SememeChronology; import sh.isaac.api.component.sememe.SememeService; import sh.isaac.api.component.sememe.version.DynamicSememe; import sh.isaac.api.component.sememe.version.MutableDescriptionSememe; import sh.isaac.api.component.sememe.version.dynamicSememe.DynamicSememeColumnInfo; import sh.isaac.api.component.sememe.version.dynamicSememe.DynamicSememeData; import sh.isaac.api.component.sememe.version.dynamicSememe.DynamicSememeUtility; import sh.isaac.api.component.sememe.version.dynamicSememe.dataTypes.DynamicSememeArray; import sh.isaac.api.constants.DynamicSememeConstants; import sh.isaac.api.constants.MetadataConceptConstant; import sh.isaac.api.constants.MetadataConceptConstantGroup; import sh.isaac.api.constants.MetadataDynamicSememeConstant; import sh.isaac.api.externalizable.DataWriterService; import sh.isaac.api.externalizable.MultipleDataWriterService; import sh.isaac.api.logic.LogicalExpression; import sh.isaac.api.logic.LogicalExpressionBuilder; import sh.isaac.api.logic.LogicalExpressionBuilderService; import sh.isaac.api.util.UuidT5Generator; import static sh.isaac.api.logic.LogicalExpressionBuilder.And; import static sh.isaac.api.logic.LogicalExpressionBuilder.ConceptAssertion; import static sh.isaac.api.logic.LogicalExpressionBuilder.NecessarySet; //~--- classes ---------------------------------------------------------------- /** * Class for programatically creating and exporting a taxonomy. * * @author kec */ @Contract public class IsaacTaxonomy { /** The concept builders. */ private final TreeMap<String, ConceptBuilder> conceptBuilders = new TreeMap<>(); /** The sememe builders. */ private final List<SememeBuilder<?>> sememeBuilders = new ArrayList<>(); /** The concept builders in insertion order. */ private final List<ConceptBuilder> conceptBuildersInInsertionOrder = new ArrayList<>(); /** The parent stack. */ private final Stack<ConceptBuilder> parentStack = new Stack<>(); /** The current. */ private ConceptBuilder current; /** The module spec. */ private final ConceptSpecification moduleSpec; /** The path spec. */ private final ConceptSpecification pathSpec; /** The author spec. */ private final ConceptSpecification authorSpec; /** The semantic tag. */ private final String semanticTag; //~--- constructors -------------------------------------------------------- /** * Instantiates a new isaac taxonomy. * * @param path the path * @param author the author * @param module the module * @param isaType the isa type * @param semanticTag the semantic tag */ public IsaacTaxonomy(ConceptSpecification path, ConceptSpecification author, ConceptSpecification module, ConceptSpecification isaType, String semanticTag) { this.pathSpec = path; this.authorSpec = author; this.moduleSpec = module; this.semanticTag = semanticTag; } //~--- methods ------------------------------------------------------------- /** * Creates the concept. * * @param cc the cc * @return the concept builder * @throws Exception the exception */ public final ConceptBuilder createConcept(MetadataConceptConstant cc) throws Exception { try { final ConceptBuilder cb = createConcept(cc.getPrimaryName(), (cc.getParent() != null) ? cc.getParent() .getConceptSequence() : null, null); cb.setPrimordialUuid(cc.getUUID()); cc.getDefinitions().forEach((definition) -> { addDescription(definition, cb, TermAux.DEFINITION_DESCRIPTION_TYPE, false); }); cc.getSynonyms().forEach((definition) -> { addDescription(definition, cb, TermAux.SYNONYM_DESCRIPTION_TYPE, false); }); if (cc instanceof MetadataConceptConstantGroup) { pushParent(current()); for (final MetadataConceptConstant nested: ((MetadataConceptConstantGroup) cc).getChildren()) { createConcept(nested); } popParent(); } if (cc instanceof MetadataDynamicSememeConstant) { // See {@link DynamicSememeUsageDescription} class for more details on this format. final MetadataDynamicSememeConstant dsc = (MetadataDynamicSememeConstant) cc; final DescriptionBuilder<? extends SememeChronology<?>, ? extends MutableDescriptionSememe<?>> db = addDescription(dsc.getSememeAssemblageDescription(), cb, TermAux.DEFINITION_DESCRIPTION_TYPE, false); // Annotate the description as the 'special' type that means this concept is suitable for use as an assemblage concept SememeBuilder<? extends SememeChronology<? extends DynamicSememe<?>>> sb = Get.sememeBuilderService() .getDynamicSememeBuilder(db, DynamicSememeConstants.get().DYNAMIC_SEMEME_DEFINITION_DESCRIPTION .getNid()); db.addSememe(sb); if (dsc.getDynamicSememeColumns() != null) { for (final DynamicSememeColumnInfo col: dsc.getDynamicSememeColumns()) { final DynamicSememeData[] colData = LookupService.getService(DynamicSememeUtility.class) .configureDynamicSememeDefinitionDataForColumn(col); sb = Get.sememeBuilderService() .getDynamicSememeBuilder(cb, DynamicSememeConstants.get().DYNAMIC_SEMEME_EXTENSION_DEFINITION .getNid(), colData); cb.addSememe(sb); } } final DynamicSememeData[] data = LookupService.getService(DynamicSememeUtility.class) .configureDynamicSememeRestrictionData( dsc.getReferencedComponentTypeRestriction(), dsc.getReferencedComponentSubTypeRestriction()); if (data != null) { sb = Get.sememeBuilderService() .getDynamicSememeBuilder(cb, DynamicSememeConstants.get().DYNAMIC_SEMEME_REFERENCED_COMPONENT_RESTRICTION .getNid(), data); cb.addSememe(sb); } final DynamicSememeArray<DynamicSememeData> indexConfig = LookupService.getService(DynamicSememeUtility.class) .configureColumnIndexInfo(dsc.getDynamicSememeColumns()); if (indexConfig != null) { sb = Get.sememeBuilderService() .getDynamicSememeBuilder(cb, DynamicSememeConstants.get().DYNAMIC_SEMEME_INDEX_CONFIGURATION .getNid(), new DynamicSememeData[] { indexConfig }); cb.addSememe(sb); } } return cb; } catch (final Exception e) { throw new Exception("Problem with '" + cc.getPrimaryName() + "'", e); } } /** * Export. * * @param jsonPath the json path * @param ibdfPath the ibdf path * @throws IOException Signals that an I/O exception has occurred. */ public void export(Optional<Path> jsonPath, Optional<Path> ibdfPath) throws IOException { final long exportTime = System.currentTimeMillis(); final int stampSequence = Get.stampService() .getStampSequence(State.ACTIVE, exportTime, this.authorSpec.getConceptSequence(), this.moduleSpec.getConceptSequence(), this.pathSpec.getConceptSequence()); final CommitService commitService = Get.commitService(); final SememeService sememeService = Get.sememeService(); final ConceptService conceptService = Get.conceptService(); commitService.setComment(stampSequence, "Generated by maven from java sources"); this.conceptBuildersInInsertionOrder.forEach((builder) -> { buildAndWrite(builder, stampSequence, conceptService, sememeService); }); this.sememeBuilders.forEach((builder) -> { buildAndWrite(builder, stampSequence, conceptService, sememeService); }); final int stampAliasForPromotion = Get.stampService() .getStampSequence(State.ACTIVE, exportTime + (1000 * 60), this.authorSpec.getConceptSequence(), this.moduleSpec.getConceptSequence(), this.pathSpec.getConceptSequence()); commitService.addAlias(stampSequence, stampAliasForPromotion, "promoted by maven"); try (DataWriterService writer = new MultipleDataWriterService(jsonPath, ibdfPath)) { Get.ochreExternalizableStream() .forEach((ochreExternalizable) -> writer.put(ochreExternalizable)); } } /** * Export java binding. * * @param out the out * @param packageName the package name * @param className the class name * @throws IOException Signals that an I/O exception has occurred. */ public void exportJavaBinding(Writer out, String packageName, String className) throws IOException { out.append("package " + packageName + ";\n"); out.append("\n\nimport sh.isaac.api.component.concept.ConceptSpecification;\n"); out.append("import sh.isaac.api.ConceptProxy;\n"); out.append("\n\npublic class " + className + " {\n"); for (final ConceptBuilder concept: this.conceptBuildersInInsertionOrder) { final String preferredName = concept.getConceptDescriptionText(); String constantName = preferredName.toUpperCase(); if ((preferredName.indexOf("(") > 0) || (preferredName.indexOf(")") > 0)) { throw new RuntimeException("The metadata concept '" + preferredName + "' contains parens, which is illegal."); } constantName = constantName.replace(" ", "_"); constantName = constantName.replace("-", "_"); constantName = constantName.replace("+", "_PLUS"); constantName = constantName.replace("/", "_AND"); out.append("\n\n /** Java binding for the concept described as <strong><em>" + preferredName + "</em></strong>;\n * identified by UUID: {@code \n * " + "<a href=\"http://localhost:8080/terminology/rest/concept/" + concept.getPrimordialUuid() + "\">\n * " + concept.getPrimordialUuid() + "</a>}.*/"); out.append("\n public static ConceptSpecification " + constantName + " ="); out.append("\n new ConceptProxy(\"" + preferredName + "\""); for (final UUID uuid: concept.getUuidList()) { out.append(",\"" + uuid.toString() + "\""); } out.append(");"); } out.append("\n}\n"); out.close(); } /** * Export yaml binding. * * @param out the out * @param packageName the package name * @param className the class name * @throws IOException Signals that an I/O exception has occurred. */ public void exportYamlBinding(Writer out, String packageName, String className) throws IOException { out.append("#YAML Bindings for " + packageName + "." + className + "\n"); // TODO use common code (when moved somewhere common) to extract the version number from the pom.xml out.append("#Generated " + new Date().toString() + "\n"); for (final ConceptBuilder concept: this.conceptBuildersInInsertionOrder) { final String preferredName = concept.getConceptDescriptionText(); String constantName = preferredName.toUpperCase(); if ((preferredName.indexOf("(") > 0) || (preferredName.indexOf(")") > 0)) { throw new RuntimeException("The metadata concept '" + preferredName + "' contains parens, which is illegal."); } constantName = constantName.replace(" ", "_"); constantName = constantName.replace("-", "_"); constantName = constantName.replace("+", "_PLUS"); constantName = constantName.replace("/", "_AND"); out.append("\n" + constantName + ":\n"); out.append(" fsn: " + preferredName + "\n"); out.append(" uuids:\n"); for (final UUID uuid: concept.getUuidList()) { out.append(" - " + uuid.toString() + "\n"); } } out.close(); } /** * Adds the path. * * @param pathAssemblageConcept the path assemblage concept * @param pathConcept the path concept */ protected final void addPath(ConceptBuilder pathAssemblageConcept, ConceptBuilder pathConcept) { this.sememeBuilders.add(Get.sememeBuilderService() .getMembershipSememeBuilder(pathConcept.getNid(), pathAssemblageConcept.getConceptSequence())); } /** * Creates the concept. * * @param specification the specification * @return the concept builder */ protected final ConceptBuilder createConcept(ConceptSpecification specification) { final ConceptBuilder builder = createConcept(specification.getConceptDescriptionText()); builder.setPrimordialUuid(specification.getUuidList() .get(0)); if (specification.getUuidList() .size() > 1) { builder.addUuids(specification.getUuidList() .subList(1, specification.getUuidList() .size()) .toArray(new UUID[0])); } return builder; } /** * Creates the concept. * * @param name the name * @return the concept builder */ protected final ConceptBuilder createConcept(String name) { return createConcept(name, null, null); } /** * Creates the concept. * * @param name the name * @param nonPreferredSynonym the non preferred synonym * @return the concept builder */ protected final ConceptBuilder createConcept(String name, String nonPreferredSynonym) { return createConcept(name, null, nonPreferredSynonym); } /** * If parent is provided, it ignores the parent stack, and uses the provided parent instead. * If parent is not provided, it uses the parentStack (if populated), otherwise, it creates * the concept without setting a parent. * * @param name the name * @param parentId the parent id * @param nonPreferredSynonym the non preferred synonym * @return the concept builder */ protected final ConceptBuilder createConcept(String name, Integer parentId, String nonPreferredSynonym) { checkConceptDescriptionText(name); if (this.parentStack.isEmpty() && (parentId == null)) { this.current = Get.conceptBuilderService() .getDefaultConceptBuilder(name, this.semanticTag, null); } else { final LogicalExpressionBuilderService expressionBuilderService = LookupService.getService(LogicalExpressionBuilderService.class); final LogicalExpressionBuilder defBuilder = expressionBuilderService.getLogicalExpressionBuilder(); NecessarySet(And(ConceptAssertion((parentId == null) ? this.parentStack.lastElement() .getNid() : parentId, defBuilder))); final LogicalExpression logicalExpression = defBuilder.build(); this.current = Get.conceptBuilderService() .getDefaultConceptBuilder(name, this.semanticTag, logicalExpression); } if (org.apache.commons.lang3.StringUtils.isNotBlank(nonPreferredSynonym)) { this.current.addDescription(nonPreferredSynonym, TermAux.SYNONYM_DESCRIPTION_TYPE); } this.conceptBuilders.put(name, this.current); this.conceptBuildersInInsertionOrder.add(this.current); return this.current; } /** * Current. * * @return the concept builder */ protected final ConceptBuilder current() { return this.current; } /** * Export. * * @param dataOutputStream the data output stream */ protected void export(DataOutputStream dataOutputStream) { throw new UnsupportedOperationException( "Not supported yet."); // To change body of generated methods, choose Tools | Templates. } /** * Iterator over all of the concept builders, and 'fix' any that were entered without having their * primordial UUID set to a consistent value. The builder assigned a Type4 (random) UUID the first time that * getPrimordialUuid() is called - must override that UUID with one that we can consistently create upon each * execution that builds the MetaData constants. */ protected final void generateStableUUIDs() { this.conceptBuilders.values().forEach((cb) -> { ensureStableUUID(cb); }); } /** * Pop parent. */ protected final void popParent() { this.parentStack.pop(); } /** * Push parent. * * @param parent the parent */ protected final void pushParent(ConceptBuilder parent) { ensureStableUUID(parent); // no generated UUIDs from this point on.... this.parentStack.push(parent); } /** * type should be either {@link TermAux#DEFINITION_DESCRIPTION_TYPE} or {@link TermAux#SYNONYM_DESCRIPTION_TYPE} * This currently only creates english language descriptions. * * @param description the description * @param cb the cb * @param descriptionType the description type * @param preferred the preferred * @return the description builder<? extends sememe chronology<?>,? extends mutable description sememe<?>> */ private DescriptionBuilder<? extends SememeChronology<?>, ? extends MutableDescriptionSememe<?>> addDescription(String description, ConceptBuilder cb, ConceptSpecification descriptionType, boolean preferred) { final DescriptionBuilder<? extends SememeChronology<?>, ? extends MutableDescriptionSememe<?>> db = LookupService.getService(DescriptionBuilderService.class) .getDescriptionBuilder(description, cb, descriptionType, TermAux.ENGLISH_LANGUAGE); if (preferred) { db.addPreferredInDialectAssemblage(TermAux.US_DIALECT_ASSEMBLAGE); } else { db.addAcceptableInDialectAssemblage(TermAux.US_DIALECT_ASSEMBLAGE); } cb.addDescription(db); return db; } /** * Builds the and write. * * @param builder the builder * @param stampCoordinate the stamp coordinate * @param conceptService the concept service * @param sememeService the sememe service * @throws IllegalStateException the illegal state exception */ private void buildAndWrite(IdentifiedComponentBuilder builder, int stampCoordinate, ConceptService conceptService, SememeService sememeService) throws IllegalStateException { final List<?> builtObjects = new ArrayList<>(); builder.build(stampCoordinate, builtObjects); builtObjects.forEach((builtObject) -> { if (builtObject instanceof ConceptChronology) { conceptService.writeConcept( (ConceptChronology<? extends ConceptVersion<?>>) builtObject); } else if (builtObject instanceof SememeChronology) { sememeService.writeSememe((SememeChronology) builtObject); } else { throw new UnsupportedOperationException("Can't handle: " + builtObject); } }); } /** * Check concept description text. * * @param name the name */ private void checkConceptDescriptionText(String name) { if (this.conceptBuilders.containsKey(name)) { throw new RuntimeException("Concept is already added"); } } /** * Ensure stable UUID. * * @param builder the builder */ private void ensureStableUUID(ConceptBuilder builder) { if (builder.getPrimordialUuid() .version() == 4) { builder.setPrimordialUuid(UuidT5Generator.get(UuidT5Generator.PATH_ID_FROM_FS_DESC, builder.getConceptDescriptionText())); } } }