/*
* 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.provider.concept;
//~--- JDK imports ------------------------------------------------------------
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.ParseException;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
//~--- non-JDK imports --------------------------------------------------------
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.glassfish.hk2.runlevel.RunLevel;
import org.jvnet.hk2.annotations.Service;
import sh.isaac.api.ConceptActiveService;
import sh.isaac.api.ConfigurationService;
import sh.isaac.api.Get;
import sh.isaac.api.LookupService;
import sh.isaac.api.SystemStatusService;
import sh.isaac.api.chronicle.LatestVersion;
import sh.isaac.api.collections.ConceptSequenceSet;
import sh.isaac.api.component.concept.ConceptChronology;
import sh.isaac.api.component.concept.ConceptService;
import sh.isaac.api.component.concept.ConceptSnapshot;
import sh.isaac.api.component.concept.ConceptSnapshotService;
import sh.isaac.api.component.concept.ConceptVersion;
import sh.isaac.api.component.sememe.SememeChronology;
import sh.isaac.api.component.sememe.version.DescriptionSememe;
import sh.isaac.api.coordinate.LanguageCoordinate;
import sh.isaac.api.coordinate.StampCoordinate;
import sh.isaac.model.concept.ConceptChronologyImpl;
import sh.isaac.model.concept.ConceptSnapshotImpl;
import sh.isaac.model.waitfree.CasSequenceObjectMap;
//~--- classes ----------------------------------------------------------------
/**
* The Class ConceptProvider.
*
* @author kec
*/
@Service
@RunLevel(value = 1)
public class ConceptProvider
implements ConceptService {
/** The Constant LOG. */
private static final Logger LOG = LogManager.getLogger();
/** The Constant CRADLE_PROPERTIES_FILE_NAME. */
public static final String CRADLE_PROPERTIES_FILE_NAME = "cradle.properties";
/** The Constant CRADLE_ID_FILE_NAME. */
public static final String CRADLE_ID_FILE_NAME = "dbid.txt";
/** The Constant CRADLE_DATA_VERSION. */
public static final String CRADLE_DATA_VERSION = "1.5";
/** The Constant CRADLE_DATA_VERSION_PROPERTY. */
public static final String CRADLE_DATA_VERSION_PROPERTY = "cradle.data.version";
//~--- fields --------------------------------------------------------------
/** The load required. */
private final AtomicBoolean loadRequired = new AtomicBoolean(true);
/** The database validity. */
private DatabaseValidity databaseValidity = DatabaseValidity.NOT_SET;
/** The db id. */
private UUID dbId = null;
/** The concept active service. */
ConceptActiveService conceptActiveService;
/** The concept map. */
final CasSequenceObjectMap<ConceptChronologyImpl> conceptMap;
/** The ochre concept path. */
private Path ochreConceptPath;
//~--- constructors --------------------------------------------------------
/**
* Instantiates a new concept provider.
*
* @throws IOException Signals that an I/O exception has occurred.
* @throws NumberFormatException the number format exception
* @throws ParseException the parse exception
*/
public ConceptProvider()
throws IOException, NumberFormatException, ParseException {
try {
final Path propertiesPath = LookupService.getService(ConfigurationService.class)
.getChronicleFolderPath()
.resolve(CRADLE_PROPERTIES_FILE_NAME);
final Path dbIdPath = LookupService.getService(ConfigurationService.class)
.getChronicleFolderPath()
.resolve(CRADLE_ID_FILE_NAME);
final Path folderPath = LookupService.getService(ConfigurationService.class)
.getChronicleFolderPath()
.resolve("ochre-concepts");
Files.createDirectories(folderPath);
LOG.info("Setting up OCHRE ConceptProvider at " + folderPath.toAbsolutePath());
final Properties cradleProps = new Properties();
if (propertiesPath.toFile()
.exists()) {
this.loadRequired.set(true);
try (FileInputStream in = new FileInputStream(propertiesPath.toFile())) {
cradleProps.load(in);
}
if (!cradleProps.getProperty(CRADLE_DATA_VERSION_PROPERTY)
.equals(CRADLE_DATA_VERSION)) {
throw new IllegalStateException("Unsupported data version: " + cradleProps);
}
if (dbIdPath.toFile()
.exists()) {
try {
this.dbId = UUID.fromString(new String(Files.readAllBytes(dbIdPath)));
} catch (final Exception e) {
throw new IllegalStateException("The " + CRADLE_ID_FILE_NAME + " file does not contain a valid UUID!",
e);
}
} else {
LOG.warn("The " + CRADLE_ID_FILE_NAME + " file is missing from the database folder - creating a new ID");
this.dbId = UUID.randomUUID();
Files.write(dbIdPath, this.dbId.toString()
.getBytes());
}
LOG.info("Reading an existing concept store at " + folderPath.toAbsolutePath() + " with the id of '" +
this.dbId.toString() + "'");
} else {
this.loadRequired.set(false);
cradleProps.put(CRADLE_DATA_VERSION_PROPERTY, CRADLE_DATA_VERSION);
try (FileOutputStream out = new FileOutputStream(propertiesPath.toFile())) {
cradleProps.store(out, CRADLE_DATA_VERSION);
}
this.dbId = UUID.randomUUID();
Files.write(dbIdPath, this.dbId.toString()
.getBytes());
LOG.info("Creating a new (empty) concept store at " + folderPath.toAbsolutePath() + " with the id of ]" +
this.dbId.toString() + "'");
}
this.ochreConceptPath = folderPath.resolve("ochre");
if (!Files.exists(this.ochreConceptPath)) {
this.databaseValidity = DatabaseValidity.MISSING_DIRECTORY;
}
this.conceptMap = new CasSequenceObjectMap<>(new ConceptSerializer(),
this.ochreConceptPath,
"seg.",
".ochre-concepts.map");
} catch (IOException | IllegalStateException e) {
LookupService.getService(SystemStatusService.class)
.notifyServiceConfigurationFailure("ChRonicled Assertion Database of Logical Expressions (OCHRE)",
e);
throw e;
}
}
//~--- methods -------------------------------------------------------------
/**
* Clear database validity value.
*/
@Override
public void clearDatabaseValidityValue() {
// Reset to enforce analysis
this.databaseValidity = DatabaseValidity.NOT_SET;
}
/**
* Write concept.
*
* @param concept the concept
*/
@Override
public void writeConcept(ConceptChronology<? extends ConceptVersion<?>> concept) {
this.conceptMap.put(concept.getConceptSequence(), (ConceptChronologyImpl) concept);
}
/**
* Start me.
*/
@PostConstruct
private void startMe() {
LOG.info("Starting OCHRE ConceptProvider post-construct");
this.conceptActiveService = LookupService.getService(ConceptActiveService.class);
if (this.loadRequired.compareAndSet(true, false)) {
LOG.info("Reading existing OCHRE concept-map.");
if (this.conceptMap.initialize()) {
this.databaseValidity = DatabaseValidity.POPULATED_DIRECTORY;
}
LOG.info("Finished OCHRE read.");
}
}
/**
* Stop me.
*/
@PreDestroy
private void stopMe() {
LOG.info("Stopping OCHRE ConceptProvider.");
LOG.info("Writing OCHRE concept-map.");
this.conceptMap.write();
}
//~--- get methods ---------------------------------------------------------
/**
* Gets the concept.
*
* @param conceptId the concept id
* @return the concept
*/
@Override
public ConceptChronologyImpl getConcept(int conceptId) {
if (conceptId < 0) {
conceptId = Get.identifierService()
.getConceptSequence(conceptId);
}
return this.conceptMap.getQuick(conceptId);
}
/**
* Gets the concept.
*
* @param conceptUuids the concept uuids
* @return the concept
*/
@Override
public ConceptChronologyImpl getConcept(UUID... conceptUuids) {
final int conceptNid = Get.identifierService()
.getNidForUuids(conceptUuids);
final int conceptSequence = Get.identifierService()
.getConceptSequence(conceptNid);
final Optional<ConceptChronologyImpl> optionalConcept = this.conceptMap.get(conceptSequence);
if (optionalConcept.isPresent()) {
return optionalConcept.get();
}
final ConceptChronologyImpl concept = new ConceptChronologyImpl(conceptUuids[0], conceptNid, conceptSequence);
if (conceptUuids.length > 1) {
concept.setAdditionalUuids(Arrays.asList(Arrays.copyOfRange(conceptUuids, 1, conceptUuids.length)));
}
this.conceptMap.put(conceptSequence, concept);
return this.conceptMap.getQuick(conceptSequence);
}
/**
* Checks for concept.
*
* @param conceptId the concept id
* @return true, if successful
*/
@Override
public boolean hasConcept(int conceptId) {
if (conceptId < 0) {
conceptId = Get.identifierService()
.getConceptSequence(conceptId);
}
return this.conceptMap.containsKey(conceptId);
}
/**
* Checks if concept active.
*
* @param conceptSequence the concept sequence
* @param stampCoordinate the stamp coordinate
* @return true, if concept active
*/
@Override
public boolean isConceptActive(int conceptSequence, StampCoordinate stampCoordinate) {
return this.conceptActiveService.isConceptActive(conceptSequence, stampCoordinate);
}
/**
* Gets the concept chronology stream.
*
* @return the concept chronology stream
*/
@Override
public Stream<ConceptChronology<? extends ConceptVersion<?>>> getConceptChronologyStream() {
return this.conceptMap.getStream().map((cc) -> {
return (ConceptChronology<? extends ConceptVersion<?>>) cc;
});
}
/**
* Gets the concept chronology stream.
*
* @param conceptSequences the concept sequences
* @return the concept chronology stream
*/
@Override
public Stream<ConceptChronology<? extends ConceptVersion<?>>> getConceptChronologyStream(
ConceptSequenceSet conceptSequences) {
return Get.identifierService().getConceptSequenceStream().filter((int sequence) -> conceptSequences.contains(sequence)).mapToObj((int sequence) -> {
final Optional<ConceptChronologyImpl> result = this.conceptMap.get(sequence);
if (result.isPresent()) {
return this.conceptMap.get(sequence)
.get();
}
throw new IllegalStateException("No concept for sequence: " + sequence);
});
}
/**
* Gets the concept count.
*
* @return the concept count
*/
@Override
public int getConceptCount() {
return this.conceptMap.getSize();
}
/**
* Gets the concept data.
*
* @param i the i
* @return the concept data
* @throws IOException Signals that an I/O exception has occurred.
*/
public Optional<ConceptChronologyImpl> getConceptData(int i)
throws IOException {
if (i < 0) {
i = Get.identifierService()
.getConceptSequence(i);
}
return this.conceptMap.get(i);
}
/**
* Gets the concept key parallel stream.
*
* @return the concept key parallel stream
*/
@Override
public IntStream getConceptKeyParallelStream() {
return this.conceptMap.getKeyParallelStream();
}
/**
* Gets the concept key stream.
*
* @return the concept key stream
*/
@Override
public IntStream getConceptKeyStream() {
return this.conceptMap.getKeyStream();
}
/**
* Gets the data store id.
*
* @return the data store id
*/
@Override
public UUID getDataStoreId() {
return this.dbId;
}
/**
* Gets the database folder.
*
* @return the database folder
*/
@Override
public Path getDatabaseFolder() {
return this.ochreConceptPath;
}
/**
* Gets the database validity status.
*
* @return the database validity status
*/
@Override
public DatabaseValidity getDatabaseValidityStatus() {
return this.databaseValidity;
}
/**
* Gets the optional concept.
*
* @param conceptId the concept id
* @return the optional concept
*/
@Override
public Optional<? extends ConceptChronology<? extends ConceptVersion<?>>> getOptionalConcept(int conceptId) {
if (conceptId < 0) {
conceptId = Get.identifierService()
.getConceptSequence(conceptId);
}
return this.conceptMap.get(conceptId);
}
/**
* Gets the optional concept.
*
* @param conceptUuids the concept uuids
* @return the optional concept
*/
@Override
public Optional<? extends ConceptChronology<? extends ConceptVersion<?>>> getOptionalConcept(UUID... conceptUuids) {
// check hasUuid first, because getOptionalConcept adds the UUID to the index if it doesn't exist...
if (Get.identifierService()
.hasUuid(conceptUuids)) {
return getOptionalConcept(Get.identifierService()
.getConceptSequenceForUuids(conceptUuids));
} else {
return Optional.empty();
}
}
/**
* Gets the parallel concept chronology stream.
*
* @return the parallel concept chronology stream
*/
@Override
public Stream<ConceptChronology<? extends ConceptVersion<?>>> getParallelConceptChronologyStream() {
return this.conceptMap.getParallelStream().map((cc) -> {
return cc;
});
}
/**
* Gets the parallel concept chronology stream.
*
* @param conceptSequences the concept sequences
* @return the parallel concept chronology stream
*/
@Override
public Stream<ConceptChronology<? extends ConceptVersion<?>>> getParallelConceptChronologyStream(
ConceptSequenceSet conceptSequences) {
return Get.identifierService().getParallelConceptSequenceStream().filter((int sequence) -> conceptSequences.contains(sequence)).mapToObj((int sequence) -> {
final Optional<ConceptChronologyImpl> result = this.conceptMap.get(sequence);
if (result.isPresent()) {
return this.conceptMap.get(sequence)
.get();
}
throw new IllegalStateException("No concept for sequence: " + sequence);
});
}
/**
* Gets the snapshot.
*
* @param stampCoordinate the stamp coordinate
* @param languageCoordinate the language coordinate
* @return the snapshot
*/
@Override
public ConceptSnapshotService getSnapshot(StampCoordinate stampCoordinate, LanguageCoordinate languageCoordinate) {
return new ConceptSnapshotProvider(stampCoordinate, languageCoordinate);
}
//~--- inner classes -------------------------------------------------------
/**
* The Class ConceptSnapshotProvider.
*/
public class ConceptSnapshotProvider
implements ConceptSnapshotService {
/** The stamp coordinate. */
StampCoordinate stampCoordinate;
/** The language coordinate. */
LanguageCoordinate languageCoordinate;
//~--- constructors -----------------------------------------------------
/**
* Instantiates a new concept snapshot provider.
*
* @param stampCoordinate the stamp coordinate
* @param languageCoordinate the language coordinate
*/
public ConceptSnapshotProvider(StampCoordinate stampCoordinate, LanguageCoordinate languageCoordinate) {
this.stampCoordinate = stampCoordinate;
this.languageCoordinate = languageCoordinate;
}
//~--- methods ----------------------------------------------------------
/**
* Concept description text.
*
* @param conceptId the concept id
* @return the string
*/
@Override
public String conceptDescriptionText(int conceptId) {
final Optional<LatestVersion<DescriptionSememe<?>>> descriptionOptional = getDescriptionOptional(conceptId);
if (descriptionOptional.isPresent()) {
return descriptionOptional.get()
.value()
.getText();
}
return "No desc for: " + conceptId;
}
/**
* To string.
*
* @return the string
*/
@Override
public String toString() {
return "ConceptSnapshotProvider{" + "stampCoordinate=" + this.stampCoordinate + ", languageCoordinate=" +
this.languageCoordinate + '}';
}
//~--- get methods ------------------------------------------------------
/**
* Checks if concept active.
*
* @param conceptSequence the concept sequence
* @return true, if concept active
*/
@Override
public boolean isConceptActive(int conceptSequence) {
return ConceptProvider.this.isConceptActive(conceptSequence, this.stampCoordinate);
}
/**
* Gets the concept snapshot.
*
* @param conceptSequence the concept sequence
* @return the concept snapshot
*/
@Override
public ConceptSnapshot getConceptSnapshot(int conceptSequence) {
return new ConceptSnapshotImpl(getConcept(conceptSequence), this.stampCoordinate, this.languageCoordinate);
}
/**
* Gets the description list.
*
* @param conceptId the concept id
* @return the description list
*/
private List<SememeChronology<? extends DescriptionSememe<?>>> getDescriptionList(int conceptId) {
final int conceptNid = Get.identifierService()
.getConceptNid(conceptId);
return Get.sememeService()
.getDescriptionsForComponent(conceptNid)
.collect(Collectors.toList());
}
/**
* Gets the description optional.
*
* @param conceptId the concept id
* @return the description optional
*/
@Override
public Optional<LatestVersion<DescriptionSememe<?>>> getDescriptionOptional(int conceptId) {
return this.languageCoordinate.getDescription(getDescriptionList(conceptId), this.stampCoordinate);
}
/**
* Gets the fully specified description.
*
* @param conceptId the concept id
* @return the fully specified description
*/
@Override
public Optional<LatestVersion<DescriptionSememe<?>>> getFullySpecifiedDescription(int conceptId) {
return this.languageCoordinate.getFullySpecifiedDescription(getDescriptionList(conceptId),
this.stampCoordinate);
}
/**
* Gets the language coordinate.
*
* @return the language coordinate
*/
@Override
public LanguageCoordinate getLanguageCoordinate() {
return this.languageCoordinate;
}
/**
* Gets the preferred description.
*
* @param conceptId the concept id
* @return the preferred description
*/
@Override
public Optional<LatestVersion<DescriptionSememe<?>>> getPreferredDescription(int conceptId) {
return this.languageCoordinate.getPreferredDescription(getDescriptionList(conceptId), this.stampCoordinate);
}
/**
* Gets the stamp coordinate.
*
* @return the stamp coordinate
*/
@Override
public StampCoordinate getStampCoordinate() {
return this.stampCoordinate;
}
}
}