/*
* 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.stamp;
//~--- JDK imports ------------------------------------------------------------
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.IntStream;
//~--- non-JDK imports --------------------------------------------------------
import javafx.concurrent.Task;
//~--- JDK imports ------------------------------------------------------------
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.ConfigurationService;
import sh.isaac.api.Get;
import sh.isaac.api.LookupService;
import sh.isaac.api.State;
import sh.isaac.api.SystemStatusService;
import sh.isaac.api.bootstrap.TermAux;
import sh.isaac.api.collections.ConcurrentObjectIntMap;
import sh.isaac.api.collections.ConcurrentSequenceSerializedObjectMap;
import sh.isaac.api.commit.Stamp;
import sh.isaac.api.commit.StampService;
import sh.isaac.api.commit.UncommittedStamp;
import sh.isaac.api.task.TimedTask;
//~--- classes ----------------------------------------------------------------
/**
* Created by kec on 1/2/16.
*/
@Service(name = "Stamp Provider")
@RunLevel(value = 1)
public class StampProvider
implements StampService {
/** The Constant LOG. */
private static final Logger LOG = LogManager.getLogger();
/** The Constant STAMP_MANAGER_DATA_FILENAME. */
private static final String STAMP_MANAGER_DATA_FILENAME = "stamp-manager.data";
/** The Constant DEFAULT_STAMP_MANAGER_FOLDER. */
public static final String DEFAULT_STAMP_MANAGER_FOLDER = "stamp-manager";
/**
* TODO: persist across restarts.
*/
private static final Map<UncommittedStamp, Integer> UNCOMMITTED_STAMP_TO_STAMP_SEQUENCE_MAP =
new ConcurrentHashMap<>();
//~--- fields --------------------------------------------------------------
/** The stamp lock. */
private final ReentrantLock stampLock = new ReentrantLock();
/** The next stamp sequence. */
private final AtomicInteger nextStampSequence = new AtomicInteger(FIRST_STAMP_SEQUENCE);
/**
* Persistent map of stamp sequences to a Stamp object.
*/
private final ConcurrentObjectIntMap<Stamp> stampMap = new ConcurrentObjectIntMap<>();
/** The load required. */
private final AtomicBoolean loadRequired = new AtomicBoolean();
/** The database validity. */
private DatabaseValidity databaseValidity = DatabaseValidity.NOT_SET;
/** The stamp sequence path sequence map. */
ConcurrentHashMap<Integer, Integer> stampSequencePathSequenceMap = new ConcurrentHashMap();
/** The db folder path. */
private final Path dbFolderPath;
/** The stamp manager folder. */
private final Path stampManagerFolder;
/**
* Persistent as a result of reading and writing the stampMap.
*/
private final ConcurrentSequenceSerializedObjectMap<Stamp> inverseStampMap;
//~--- constructors --------------------------------------------------------
/**
* Instantiates a new stamp provider.
*
* @throws IOException Signals that an I/O exception has occurred.
*/
public StampProvider()
throws IOException {
this.dbFolderPath = LookupService.getService(ConfigurationService.class)
.getChronicleFolderPath()
.resolve("stamp-provider");
this.loadRequired.set(Files.exists(this.dbFolderPath));
Files.createDirectories(this.dbFolderPath);
this.inverseStampMap = new ConcurrentSequenceSerializedObjectMap<>(new StampSerializer(),
this.dbFolderPath,
null,
null);
this.stampManagerFolder = this.dbFolderPath.resolve(DEFAULT_STAMP_MANAGER_FOLDER);
if (!Files.exists(this.stampManagerFolder)) {
this.databaseValidity = DatabaseValidity.MISSING_DIRECTORY;
}
Files.createDirectories(this.stampManagerFolder);
}
//~--- methods -------------------------------------------------------------
/**
* Adds the stamp.
*
* @param stamp the stamp
* @param stampSequence the stamp sequence
*/
@Override
public void addStamp(Stamp stamp, int stampSequence) {
this.stampMap.put(stamp, stampSequence);
this.inverseStampMap.put(stampSequence, stamp);
}
/**
* Cancel.
*
* @param authorSequence the author sequence
* @return the task
*/
@Override
public synchronized Task<Void> cancel(int authorSequence) {
UNCOMMITTED_STAMP_TO_STAMP_SEQUENCE_MAP.forEach((uncommittedStamp, stampSequence) -> {
// for each uncommitted stamp matching the author, remove the uncommitted stamp
// and replace with a canceled stamp.
if (uncommittedStamp.authorSequence == authorSequence) {
final Stamp stamp = new Stamp(uncommittedStamp.status,
Long.MIN_VALUE,
uncommittedStamp.authorSequence,
uncommittedStamp.moduleSequence,
uncommittedStamp.pathSequence);
addStamp(stamp, stampSequence);
UNCOMMITTED_STAMP_TO_STAMP_SEQUENCE_MAP.remove(uncommittedStamp);
}
});
// TODO make asynchronous with a actual task.
final Task<Void> task = new TimedTask() {
@Override
protected Object call()
throws Exception {
Get.activeTasks()
.remove(this);
return null;
}
};
Get.activeTasks()
.add(task);
Get.workExecutors()
.getExecutor()
.execute(task);
return task;
}
/**
* Clear database validity value.
*/
@Override
public void clearDatabaseValidityValue() {
// Reset to enforce analysis
this.databaseValidity = DatabaseValidity.NOT_SET;
}
/**
* Describe stamp sequence.
*
* @param stampSequence the stamp sequence
* @return the string
*/
@Override
public String describeStampSequence(int stampSequence) {
final StringBuilder sb = new StringBuilder();
sb.append("{Stamp≤");
sb.append(stampSequence);
sb.append("::");
final State status = getStatusForStamp(stampSequence);
sb.append(status);
if (status == State.ACTIVE) {
sb.append(" ");
}
sb.append(" ");
final long time = getTimeForStamp(stampSequence);
if (time == Long.MAX_VALUE) {
sb.append("UNCOMMITTED:");
} else if (time == Long.MIN_VALUE) {
sb.append("CANCELED: ");
} else {
sb.append(Instant.ofEpochMilli(time));
}
sb.append(" a:");
sb.append(Get.conceptDescriptionText(getAuthorSequenceForStamp(stampSequence)));
sb.append(" <");
sb.append(getAuthorSequenceForStamp(stampSequence));
sb.append(">");
sb.append(" m:");
sb.append(Get.conceptDescriptionText(getModuleSequenceForStamp(stampSequence)));
sb.append(" <");
sb.append(getModuleSequenceForStamp(stampSequence));
sb.append(">");
sb.append(" p: ");
sb.append(Get.conceptDescriptionText(getPathSequenceForStamp(stampSequence)));
sb.append(" <");
sb.append(getPathSequenceForStamp(stampSequence));
sb.append(">≥S}");
return sb.toString();
}
/**
* Stamp sequences equal except author and time.
*
* @param stampSequence1 the stamp sequence 1
* @param stampSequence2 the stamp sequence 2
* @return true, if successful
*/
@Override
public boolean stampSequencesEqualExceptAuthorAndTime(int stampSequence1, int stampSequence2) {
if (getModuleNidForStamp(stampSequence1) != getModuleNidForStamp(stampSequence2)) {
return false;
}
if (getPathNidForStamp(stampSequence1) != getPathNidForStamp(stampSequence2)) {
return false;
}
return getStatusForStamp(stampSequence1) == getStatusForStamp(stampSequence2);
}
/**
* Start me.
*/
@PostConstruct
private void startMe() {
try {
LOG.info("Starting StampProvider post-construct");
if (this.loadRequired.get()) {
LOG.info("Reading existing commit manager data. ");
LOG.info("Reading " + STAMP_MANAGER_DATA_FILENAME);
try (DataInputStream in = new DataInputStream(new FileInputStream(new File(this.stampManagerFolder.toFile(),
STAMP_MANAGER_DATA_FILENAME)))) {
this.nextStampSequence.set(in.readInt());
final int stampMapSize = in.readInt();
for (int i = 0; i < stampMapSize; i++) {
final int stampSequence = in.readInt();
final Stamp stamp = new Stamp(in);
this.stampMap.put(stamp, stampSequence);
this.inverseStampMap.put(stampSequence, stamp);
}
final int uncommittedSize = in.readInt();
for (int i = 0; i < uncommittedSize; i++) {
UNCOMMITTED_STAMP_TO_STAMP_SEQUENCE_MAP.put(new UncommittedStamp(in), in.readInt());
}
}
this.databaseValidity = DatabaseValidity.POPULATED_DIRECTORY;
}
} catch (final Exception e) {
LookupService.getService(SystemStatusService.class)
.notifyServiceConfigurationFailure("Stamp Provider", e);
throw new RuntimeException(e);
}
}
/**
* Stop me.
*/
@PreDestroy
private void stopMe() {
LOG.info("Stopping StampProvider pre-destroy. ");
try (DataOutputStream out = new DataOutputStream(new FileOutputStream(new File(this.stampManagerFolder.toFile(),
STAMP_MANAGER_DATA_FILENAME)))) {
out.writeInt(this.nextStampSequence.get());
out.writeInt(this.stampMap.size());
this.stampMap.forEachPair((Stamp stamp,
int stampSequence) -> {
try {
out.writeInt(stampSequence);
stamp.write(out);
} catch (final IOException ex) {
throw new RuntimeException(ex);
}
});
final int size = UNCOMMITTED_STAMP_TO_STAMP_SEQUENCE_MAP.size();
out.writeInt(size);
for (final Map.Entry<UncommittedStamp, Integer> entry: UNCOMMITTED_STAMP_TO_STAMP_SEQUENCE_MAP.entrySet()) {
entry.getKey()
.write(out);
out.writeInt(entry.getValue());
}
} catch (final IOException e) {
throw new RuntimeException(e);
}
}
//~--- get methods ---------------------------------------------------------
/**
* Gets the activated stamp sequence.
*
* @param stampSequence the stamp sequence
* @return the activated stamp sequence
*/
@Override
public int getActivatedStampSequence(int stampSequence) {
return getStampSequence(State.ACTIVE,
getTimeForStamp(stampSequence),
getAuthorSequenceForStamp(stampSequence),
getModuleSequenceForStamp(stampSequence),
getPathSequenceForStamp(stampSequence));
}
/**
* Gets the author nid for stamp.
*
* @param stampSequence the stamp sequence
* @return the author nid for stamp
*/
public int getAuthorNidForStamp(int stampSequence) {
if (stampSequence < 0) {
return TermAux.USER.getNid();
}
final Optional<Stamp> s = this.inverseStampMap.get(stampSequence);
if (s.isPresent()) {
return s.get()
.getAuthorSequence();
}
throw new NoSuchElementException("No stampSequence found: " + stampSequence);
}
/**
* Gets the author sequence for stamp.
*
* @param stampSequence the stamp sequence
* @return the author sequence for stamp
*/
@Override
public int getAuthorSequenceForStamp(int stampSequence) {
if (stampSequence < 0) {
return TermAux.USER.getConceptSequence();
}
final Optional<Stamp> s = this.inverseStampMap.get(stampSequence);
if (s.isPresent()) {
return Get.identifierService()
.getConceptSequence(s.get()
.getAuthorSequence());
}
throw new NoSuchElementException("No stampSequence found: " + stampSequence);
}
/**
* Gets the database folder.
*
* @return the database folder
*/
@Override
public Path getDatabaseFolder() {
return this.stampManagerFolder;
}
/**
* Gets the database validity status.
*
* @return the database validity status
*/
@Override
public DatabaseValidity getDatabaseValidityStatus() {
return this.databaseValidity;
}
/**
* Gets the module nid for stamp.
*
* @param stampSequence the stamp sequence
* @return the module nid for stamp
*/
private int getModuleNidForStamp(int stampSequence) {
if (stampSequence < 0) {
return TermAux.UNSPECIFIED_MODULE.getNid();
}
final Optional<Stamp> s = this.inverseStampMap.get(stampSequence);
if (s.isPresent()) {
return s.get()
.getModuleSequence();
}
throw new NoSuchElementException("No stampSequence found: " + stampSequence);
}
/**
* Gets the module sequence for stamp.
*
* @param stampSequence the stamp sequence
* @return the module sequence for stamp
*/
@Override
public int getModuleSequenceForStamp(int stampSequence) {
if (stampSequence < 0) {
return TermAux.UNSPECIFIED_MODULE.getConceptSequence();
}
final Optional<Stamp> s = this.inverseStampMap.get(stampSequence);
if (s.isPresent()) {
return Get.identifierService()
.getConceptSequence(s.get()
.getModuleSequence());
}
throw new NoSuchElementException("No stampSequence found: " + stampSequence);
}
/**
* Checks if not canceled.
*
* @param stamp the stamp
* @return true, if not canceled
*/
@Override
public boolean isNotCanceled(int stamp) {
if (stamp < 0) {
return false;
}
return getTimeForStamp(stamp) != Long.MIN_VALUE;
}
/**
* Gets the path nid for stamp.
*
* @param stampSequence the stamp sequence
* @return the path nid for stamp
*/
private int getPathNidForStamp(int stampSequence) {
if (stampSequence < 0) {
return TermAux.PATH.getNid();
}
final Optional<Stamp> s = this.inverseStampMap.get(stampSequence);
if (s.isPresent()) {
return s.get()
.getPathSequence();
}
throw new NoSuchElementException("No stampSequence found: " + stampSequence);
}
/**
* Gets the path sequence for stamp.
*
* @param stampSequence the stamp sequence
* @return the path sequence for stamp
*/
@Override
public int getPathSequenceForStamp(int stampSequence) {
if (stampSequence < 0) {
return TermAux.DEVELOPMENT_PATH.getConceptSequence();
}
if (this.stampSequencePathSequenceMap.containsKey(stampSequence)) {
return this.stampSequencePathSequenceMap.get(stampSequence);
}
final Optional<Stamp> s = this.inverseStampMap.get(stampSequence);
if (s.isPresent()) {
this.stampSequencePathSequenceMap.put(stampSequence,
Get.identifierService()
.getConceptSequence(s.get()
.getPathSequence()));
return this.stampSequencePathSequenceMap.get(stampSequence);
}
throw new NoSuchElementException("No stampSequence found: " + stampSequence);
}
/**
* Gets the pending stamps for commit.
*
* @return the pending stamps for commit
*/
@Override
synchronized public Map<UncommittedStamp, Integer> getPendingStampsForCommit() {
final Map<UncommittedStamp, Integer> pendingStampsForCommit =
new HashMap<>(UNCOMMITTED_STAMP_TO_STAMP_SEQUENCE_MAP);
UNCOMMITTED_STAMP_TO_STAMP_SEQUENCE_MAP.clear();
return pendingStampsForCommit;
}
//~--- set methods ---------------------------------------------------------
/**
* Set pending stamps for commit.
*
* @param pendingStamps the pending stamps
*/
@Override
synchronized public void setPendingStampsForCommit(Map<UncommittedStamp, Integer> pendingStamps) {
UNCOMMITTED_STAMP_TO_STAMP_SEQUENCE_MAP.putAll(pendingStamps);
}
//~--- get methods ---------------------------------------------------------
/**
* Gets the retired stamp sequence.
*
* @param stampSequence the stamp sequence
* @return the retired stamp sequence
*/
@Override
public int getRetiredStampSequence(int stampSequence) {
return getStampSequence(State.INACTIVE,
getTimeForStamp(stampSequence),
getAuthorSequenceForStamp(stampSequence),
getModuleSequenceForStamp(stampSequence),
getPathSequenceForStamp(stampSequence));
}
/**
* Gets the stamp sequence.
*
* @param status the status
* @param time the time
* @param authorSequence the author sequence
* @param moduleSequence the module sequence
* @param pathSequence the path sequence
* @return the stamp sequence
*/
@Override
public int getStampSequence(State status, long time, int authorSequence, int moduleSequence, int pathSequence) {
final Stamp stampKey = new Stamp(status, time, authorSequence, moduleSequence, pathSequence);
if (time == Long.MAX_VALUE) {
final UncommittedStamp usp = new UncommittedStamp(status, authorSequence, moduleSequence, pathSequence);
final Integer temp = UNCOMMITTED_STAMP_TO_STAMP_SEQUENCE_MAP.get(usp);
if (temp != null) {
return temp.intValue();
} else {
this.stampLock.lock();
try {
if (UNCOMMITTED_STAMP_TO_STAMP_SEQUENCE_MAP.containsKey(usp)) {
return UNCOMMITTED_STAMP_TO_STAMP_SEQUENCE_MAP.get(usp);
}
final int stampSequence = this.nextStampSequence.getAndIncrement();
UNCOMMITTED_STAMP_TO_STAMP_SEQUENCE_MAP.put(usp, stampSequence);
this.inverseStampMap.put(stampSequence, stampKey);
return stampSequence;
} finally {
this.stampLock.unlock();
}
}
}
OptionalInt stampValue = this.stampMap.get(stampKey);
if (!stampValue.isPresent()) {
// maybe have a few available in an atomic queue, and put back
// if not used? Maybe in a thread-local?
// Have different sequences, and have the increments be equal to the
// number of sequences?
this.stampLock.lock();
try {
stampValue = this.stampMap.get(stampKey);
if (!stampValue.isPresent()) {
stampValue = OptionalInt.of(this.nextStampSequence.getAndIncrement());
this.inverseStampMap.put(stampValue.getAsInt(), stampKey);
this.stampMap.put(stampKey, stampValue.getAsInt());
}
} finally {
this.stampLock.unlock();
}
}
return stampValue.getAsInt();
}
/**
* Gets the stamp sequences.
*
* @return the stamp sequences
*/
@Override
public IntStream getStampSequences() {
return IntStream.rangeClosed(FIRST_STAMP_SEQUENCE, this.nextStampSequence.get())
.filter((stampSequence) -> this.inverseStampMap.containsKey(stampSequence));
}
/**
* Gets the status for stamp.
*
* @param stampSequence the stamp sequence
* @return the status for stamp
*/
@Override
public State getStatusForStamp(int stampSequence) {
if (stampSequence < 0) {
return State.CANCELED;
}
final Optional<Stamp> s = this.inverseStampMap.get(stampSequence);
if (s.isPresent()) {
return s.get()
.getStatus();
}
throw new NoSuchElementException("No stampSequence found: " + stampSequence);
}
/**
* Gets the time for stamp.
*
* @param stampSequence the stamp sequence
* @return the time for stamp
*/
@Override
public long getTimeForStamp(int stampSequence) {
if (stampSequence < 0) {
return Long.MIN_VALUE;
}
final Optional<Stamp> s = this.inverseStampMap.get(stampSequence);
if (s.isPresent()) {
return s.get()
.getTime();
}
throw new NoSuchElementException("No stampSequence found: " + stampSequence + " map size: " +
this.stampMap.size() + " inverse map size: " + this.inverseStampMap.getSize());
}
/**
* Checks if uncommitted.
*
* @param stampSequence the stamp sequence
* @return true, if uncommitted
*/
@Override
public boolean isUncommitted(int stampSequence) {
return getTimeForStamp(stampSequence) == Long.MAX_VALUE;
}
}