/*
* 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.snapshot.calculator;
//~--- JDK imports ------------------------------------------------------------
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.ObjIntConsumer;
import java.util.stream.IntStream;
import javax.inject.Singleton;
//~--- non-JDK imports --------------------------------------------------------
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.mahout.math.map.OpenIntObjectHashMap;
import org.jvnet.hk2.annotations.Service;
import org.roaringbitmap.RoaringBitmap;
import sh.isaac.api.Get;
import sh.isaac.api.OchreCache;
import sh.isaac.api.State;
import sh.isaac.api.chronicle.LatestVersion;
import sh.isaac.api.chronicle.ObjectChronology;
import sh.isaac.api.collections.StampSequenceSet;
import sh.isaac.api.coordinate.StampCoordinate;
import sh.isaac.api.coordinate.StampPosition;
import sh.isaac.api.coordinate.StampPrecedence;
import sh.isaac.api.identity.StampedVersion;
import sh.isaac.api.observable.ObservableChronology;
import sh.isaac.api.observable.ObservableVersion;
//~--- classes ----------------------------------------------------------------
/**
* The Class RelativePositionCalculator.
*
* @author kec
*/
@Service
@Singleton // Singleton from the perspective of HK2 managed instances
public class RelativePositionCalculator
implements OchreCache {
/** The Constant log. */
private static final Logger log = LogManager.getLogger();
/** The Constant CALCULATOR_CACHE. */
private static final ConcurrentHashMap<StampCoordinate, RelativePositionCalculator> CALCULATOR_CACHE =
new ConcurrentHashMap<>();
//~--- fields --------------------------------------------------------------
/** The error count. */
private int errorCount = 0;
/** The coordinate. */
StampCoordinate coordinate;
/**
* Mapping from pathNid to each segment for that pathNid. There is one entry
* for each path reachable antecedent to the destination position of the
* computer.
*/
OpenIntObjectHashMap<Segment> pathSequenceSegmentMap;
//~--- constructors --------------------------------------------------------
/**
* Instantiates a new relative position calculator.
*/
public RelativePositionCalculator() {
// No arg constructor for HK2 managed instance
}
/**
* Instantiates a new relative position calculator.
*
* @param coordinate the coordinate
*/
public RelativePositionCalculator(StampCoordinate coordinate) {
this.coordinate = coordinate;
this.pathSequenceSegmentMap = setupPathSequenceSegmentMap(coordinate.getStampPosition());
}
//~--- methods -------------------------------------------------------------
/**
* Fast relative position.
*
* @param stampSequence1 the stamp sequence 1
* @param stampSequence2 the stamp sequence 2
* @param precedencePolicy the precedence policy
* @return the relative position
*/
public RelativePosition fastRelativePosition(int stampSequence1,
int stampSequence2,
StampPrecedence precedencePolicy) {
final long ss1Time = Get.stampService()
.getTimeForStamp(stampSequence1);
final int ss1ModuleSequence = Get.stampService()
.getModuleSequenceForStamp(stampSequence1);
final int ss1PathSequence = Get.stampService()
.getPathSequenceForStamp(stampSequence1);
final long ss2Time = Get.stampService()
.getTimeForStamp(stampSequence2);
final int ss2ModuleSequence = Get.stampService()
.getModuleSequenceForStamp(stampSequence2);
final int ss2PathSequence = Get.stampService()
.getPathSequenceForStamp(stampSequence2);
if (ss1PathSequence == ss2PathSequence) {
final Segment seg = this.pathSequenceSegmentMap.get(ss1PathSequence);
if (seg.containsPosition(ss1PathSequence, ss1ModuleSequence, ss1Time) &&
seg.containsPosition(ss2PathSequence, ss2ModuleSequence, ss2Time)) {
if (ss1Time < ss2Time) {
return RelativePosition.BEFORE;
}
if (ss1Time > ss2Time) {
return RelativePosition.AFTER;
}
if (ss1Time == ss2Time) {
return RelativePosition.EQUAL;
}
}
return RelativePosition.UNREACHABLE;
}
final Segment seg1 = this.pathSequenceSegmentMap.get(ss1PathSequence);
final Segment seg2 = this.pathSequenceSegmentMap.get(ss2PathSequence);
if ((seg1 == null) || (seg2 == null)) {
return RelativePosition.UNREACHABLE;
}
if (!(seg1.containsPosition(ss1PathSequence, ss1ModuleSequence, ss1Time) &&
seg2.containsPosition(ss2PathSequence, ss2ModuleSequence, ss2Time))) {
return RelativePosition.UNREACHABLE;
}
if (precedencePolicy == StampPrecedence.TIME) {
if (ss1Time < ss2Time) {
return RelativePosition.BEFORE;
}
if (ss1Time > ss2Time) {
return RelativePosition.AFTER;
}
if (ss1Time == ss2Time) {
return RelativePosition.EQUAL;
}
}
if (seg1.precedingSegments.contains(seg2.segmentSequence)) {
return RelativePosition.BEFORE;
}
if (seg2.precedingSegments.contains(seg1.segmentSequence)) {
return RelativePosition.AFTER;
}
return RelativePosition.CONTRADICTION;
}
/**
* Fast relative position.
*
* @param v1 the v 1
* @param v2 the v 2
* @param precedencePolicy the precedence policy
* @return the relative position
*/
public RelativePosition fastRelativePosition(StampedVersion v1,
StampedVersion v2,
StampPrecedence precedencePolicy) {
if (v1.getPathSequence() == v2.getPathSequence()) {
final Segment seg = this.pathSequenceSegmentMap.get(v1.getPathSequence());
if (seg == null) {
final StringBuilder builder = new StringBuilder();
builder.append("Segment cannot be null.");
builder.append("\nv1: ")
.append(v1);
builder.append("\nv2: ")
.append(v1);
builder.append("\nno segment in map: ")
.append(this.pathSequenceSegmentMap);
throw new IllegalStateException(builder.toString());
}
if (seg.containsPosition(v1.getPathSequence(), v1.getModuleSequence(), v1.getTime()) &&
seg.containsPosition(v2.getPathSequence(), v2.getModuleSequence(), v2.getTime())) {
if (v1.getTime() < v2.getTime()) {
return RelativePosition.BEFORE;
}
if (v1.getTime() > v2.getTime()) {
return RelativePosition.AFTER;
}
if (v1.getTime() == v2.getTime()) {
return RelativePosition.EQUAL;
}
}
return RelativePosition.UNREACHABLE;
}
final Segment seg1 = this.pathSequenceSegmentMap.get(v1.getPathSequence());
final Segment seg2 = this.pathSequenceSegmentMap.get(v2.getPathSequence());
if ((seg1 == null) || (seg2 == null)) {
return RelativePosition.UNREACHABLE;
}
if (!(seg1.containsPosition(v1.getPathSequence(), v1.getModuleSequence(), v1.getTime()) &&
seg2.containsPosition(v2.getPathSequence(), v2.getModuleSequence(), v2.getTime()))) {
return RelativePosition.UNREACHABLE;
}
if (precedencePolicy == StampPrecedence.TIME) {
if (v1.getTime() < v2.getTime()) {
return RelativePosition.BEFORE;
}
if (v1.getTime() > v2.getTime()) {
return RelativePosition.AFTER;
}
if (v1.getTime() == v2.getTime()) {
return RelativePosition.EQUAL;
}
}
if (seg1.precedingSegments.contains(seg2.segmentSequence)) {
return RelativePosition.BEFORE;
}
if (seg2.precedingSegments.contains(seg1.segmentSequence)) {
return RelativePosition.AFTER;
}
return RelativePosition.CONTRADICTION;
}
/**
* On route.
*
* @param stampSequence the stamp sequence
* @return true, if successful
*/
public boolean onRoute(int stampSequence) {
final Segment seg = this.pathSequenceSegmentMap.get(Get.stampService()
.getPathSequenceForStamp(stampSequence));
if (seg != null) {
return seg.containsPosition(Get.stampService()
.getPathSequenceForStamp(stampSequence),
Get.stampService()
.getModuleSequenceForStamp(stampSequence),
Get.stampService()
.getTimeForStamp(stampSequence));
}
return false;
}
/**
* On route.
*
* @param v the v
* @return true, if successful
*/
public boolean onRoute(StampedVersion v) {
final Segment seg = this.pathSequenceSegmentMap.get(v.getPathSequence());
if (seg != null) {
return seg.containsPosition(v.getPathSequence(), v.getModuleSequence(), v.getTime());
}
return false;
}
/**
* Relative position.
*
* @param stampSequence1 the stamp sequence 1
* @param stampSequence2 the stamp sequence 2
* @return the relative position
*/
public RelativePosition relativePosition(int stampSequence1, int stampSequence2) {
if (!(onRoute(stampSequence1) && onRoute(stampSequence2))) {
return RelativePosition.UNREACHABLE;
}
return fastRelativePosition(stampSequence1, stampSequence2, StampPrecedence.PATH);
}
/**
* Relative position.
*
* @param v1 the v 1
* @param v2 the v 2
* @return the relative position
*/
public RelativePosition relativePosition(StampedVersion v1, StampedVersion v2) {
if (!(onRoute(v1) && onRoute(v2))) {
return RelativePosition.UNREACHABLE;
}
return fastRelativePosition(v1, v2, StampPrecedence.PATH);
}
/**
* Reset.
*/
@Override
public void reset() {
log.info("Resetting RelativePositionCalculator.");
CALCULATOR_CACHE.clear();
}
/**
* To string.
*
* @return the string
*/
@Override
public String toString() {
return "RelativePositionCalculator{" + this.coordinate + '}';
}
/**
* Adds the origins to path sequence segment map.
*
* @param destination the destination
* @param pathNidSegmentMap the path nid segment map
* @param segmentSequence the segment sequence
* @param precedingSegments the preceding segments
*/
// recursively called method
private void addOriginsToPathSequenceSegmentMap(StampPosition destination,
OpenIntObjectHashMap<Segment> pathNidSegmentMap,
AtomicInteger segmentSequence,
RoaringBitmap precedingSegments) {
final Segment segment = new Segment(segmentSequence.getAndIncrement(),
destination.getStampPathSequence(),
destination.getTime(),
precedingSegments);
// precedingSegments is cumulative, each recursive call adds another
precedingSegments.add(segment.segmentSequence);
pathNidSegmentMap.put(destination.getStampPathSequence(), segment);
destination.getStampPath().getPathOrigins().stream().forEach((origin) -> {
// Recursive call
addOriginsToPathSequenceSegmentMap(
origin, pathNidSegmentMap, segmentSequence, precedingSegments);
});
}
/**
* Handle part.
*
* @param <V> the value type
* @param partsForPosition the parts for position
* @param part the part
*/
private <V extends StampedVersion> void handlePart(HashSet<V> partsForPosition, V part) {
// create a list of values so we don't have any
// concurrent modification issues with removing/adding
// items to the partsForPosition.
final List<V> partsToCompare = new ArrayList<>(partsForPosition);
for (final V prevPartToTest: partsToCompare) {
switch (fastRelativePosition(part, prevPartToTest, this.coordinate.getStampPrecedence())) {
case AFTER:
partsForPosition.remove(prevPartToTest);
partsForPosition.add(part);
break;
case BEFORE:
break;
case CONTRADICTION:
partsForPosition.add(part);
break;
case EQUAL:
// Can only have one part per time/path
// combination.
if (prevPartToTest.equals(part)) {
// part already added from another position.
// No need to add again.
break;
}
// Duplicate values encountered.
this.errorCount++;
if (this.errorCount < 5) {
log.warn("{} should never happen. " +
"Data is malformed. stampSequence: {} Part:\n{} \n Part to test: \n{}",
new Object[] { RelativePosition.EQUAL, part.getStampSequence(), part, prevPartToTest });
}
break;
case UNREACHABLE:
// Should have failed mapper.onRoute(part)
// above.
throw new RuntimeException(RelativePosition.UNREACHABLE + " should never happen.");
}
}
}
/**
* Handle stamp.
*
* @param stampsForPosition the stamps for position
* @param stampSequence the stamp sequence
*/
private void handleStamp(StampSequenceSet stampsForPosition, int stampSequence) {
if (!onRoute(stampSequence)) {
return;
}
if (stampsForPosition.isEmpty()) {
stampsForPosition.add(stampSequence);
return;
}
// create a list of values so we don't have any
// concurrent modification issues with removing/adding
// items to the stampsForPosition.
final StampSequenceSet stampsToCompare = StampSequenceSet.of(stampsForPosition);
stampsToCompare.stream().forEach((prevStamp) -> {
switch (
fastRelativePosition(
stampSequence, prevStamp, this.coordinate.getStampPrecedence())) {
case AFTER:
stampsForPosition.remove(prevStamp);
stampsForPosition.add(stampSequence);
break;
case BEFORE:
break;
case CONTRADICTION:
stampsForPosition.add(stampSequence);
break;
case EQUAL:
// Can only have one stampSequence per time/path
// combination.
if (prevStamp == stampSequence) {
// stampSequence already added from another position.
// No need to add again.
break;
}
// Duplicate values encountered.
this.errorCount++;
if (this.errorCount < 20) {
log.warn("{} should never happen. " +
"\n Data is malformed. stamp: {} Part to test: {}",
new Object[] { RelativePosition.EQUAL, stampSequence, prevStamp });
}
break;
case UNREACHABLE:
// nothing to do...
break;
default:
throw new UnsupportedOperationException("Can't handle: " +
fastRelativePosition(stampSequence,
prevStamp,
this.coordinate.getStampPrecedence()));
}
});
}
/**
* Setup path sequence segment map.
*
* @param destination the destination
* @return the open int object hash map
*/
private OpenIntObjectHashMap<Segment> setupPathSequenceSegmentMap(StampPosition destination) {
final OpenIntObjectHashMap<Segment> pathSequenceSegmentMapToSetup = new OpenIntObjectHashMap<>();
final AtomicInteger segmentSequence = new AtomicInteger(0);
// the sequence of the preceding segments is set in the recursive
// call.
final RoaringBitmap precedingSegments = new RoaringBitmap();
// call to recursive method...
addOriginsToPathSequenceSegmentMap(destination,
pathSequenceSegmentMapToSetup,
segmentSequence,
precedingSegments);
return pathSequenceSegmentMapToSetup;
}
//~--- get methods ---------------------------------------------------------
/**
* Gets the calculator.
*
* @param coordinate the coordinate
* @return the calculator
*/
public static RelativePositionCalculator getCalculator(StampCoordinate coordinate) {
RelativePositionCalculator calculator = CALCULATOR_CACHE.get(coordinate);
if (calculator != null) {
return calculator;
}
calculator = new RelativePositionCalculator(coordinate);
final RelativePositionCalculator existing = CALCULATOR_CACHE.putIfAbsent(coordinate, calculator);
if (existing != null) {
calculator = existing;
}
return calculator;
}
/**
* Gets the destination.
*
* @return the destination
*/
public StampPosition getDestination() {
return this.coordinate.getStampPosition();
}
// private class StampSequenceSetSupplier implements Supplier<StampSequenceSet> {
// @Override
// public StampSequenceSet get() {
// return new StampSequenceSet();
// }
// };
/**
* Checks if latest active.
*
* @param stampSequences A stream of stampSequences from which the latest is
* found, and then tested to determine if the latest is active.
* @return true if any of the latest stampSequences (may be multiple in the
* case of a contradiction) are active.
*/
public boolean isLatestActive(IntStream stampSequences) {
return Arrays.stream(getLatestStampSequencesAsArray(stampSequences))
.anyMatch((int stampSequence) -> Get.stampService()
.getStatusForStamp(stampSequence) == State.ACTIVE);
}
/**
* Gets the latest stamp sequences as array.
*
* @param stampSequenceStream the stamp sequence stream
* @return the latest stamp sequences as array
*/
public int[] getLatestStampSequencesAsArray(IntStream stampSequenceStream) {
return getLatestStampSequencesAsSet(stampSequenceStream).asArray();
}
/**
* Gets the latest stamp sequences as set.
*
* @param stampSequenceStream the stamp sequence stream
* @return the latest stamp sequences as set
*/
public StampSequenceSet getLatestStampSequencesAsSet(IntStream stampSequenceStream) {
final StampSequenceSet result = stampSequenceStream.collect(StampSequenceSet::new,
new LatestStampAccumulator(),
new LatestStampCombiner());
return StampSequenceSet.of(result.stream().filter((stampSequence) -> {
return this.coordinate.getAllowedStates()
.contains(Get.stampService()
.getStatusForStamp(stampSequence));
}));
}
/**
* Gets the latest version.
*
* @param <C> the generic type
* @param <V> the value type
* @param chronicle the chronicle
* @return the latest version
*/
public <C extends ObservableChronology<V>,
V extends ObservableVersion> Optional<LatestVersion<V>> getLatestVersion(C chronicle) {
final HashSet<V> latestVersionSet = new HashSet<>();
chronicle.getVersionList()
.stream()
.filter((newVersionToTest) -> (newVersionToTest.getTime() != Long.MIN_VALUE))
.filter((newVersionToTest) -> (onRoute(newVersionToTest)))
.forEach((newVersionToTest) -> {
if (latestVersionSet.isEmpty()) {
latestVersionSet.add(newVersionToTest);
} else {
handlePart(latestVersionSet, newVersionToTest);
}
});
final List<V> latestVersionList = new ArrayList<>(latestVersionSet);
if (latestVersionList.isEmpty()) {
return Optional.empty();
}
if (latestVersionList.size() == 1) {
return Optional.of(new LatestVersion<>(latestVersionList.get(0)));
}
return Optional.of(new LatestVersion<>(latestVersionList.get(0),
latestVersionList.subList(1, latestVersionList.size())));
}
/**
* Gets the latest version.
*
* @param <C> the generic type
* @param <V> the value type
* @param chronicle the chronicle
* @return the latest version
*/
public <C extends ObjectChronology<V>,
V extends StampedVersion> Optional<LatestVersion<V>> getLatestVersion(C chronicle) {
final HashSet<V> latestVersionSet = new HashSet<>();
chronicle.getVersionList()
.stream()
.filter((newVersionToTest) -> (newVersionToTest.getTime() != Long.MIN_VALUE))
.filter((newVersionToTest) -> (onRoute(newVersionToTest)))
.forEach((newVersionToTest) -> {
if (latestVersionSet.isEmpty()) {
latestVersionSet.add(newVersionToTest);
} else {
handlePart(latestVersionSet, newVersionToTest);
}
});
if (this.coordinate.getAllowedStates()
.equals(State.ACTIVE_ONLY_SET)) {
final HashSet<V> inactiveVersions = new HashSet<>();
latestVersionSet.stream().forEach((version) -> {
if (version.getState() != State.ACTIVE) {
inactiveVersions.add(version);
}
});
latestVersionSet.removeAll(inactiveVersions);
}
final List<V> latestVersionList = new ArrayList<>(latestVersionSet);
if (latestVersionList.isEmpty()) {
return Optional.empty();
}
if (latestVersionList.size() == 1) {
return Optional.of(new LatestVersion<>(latestVersionList.get(0)));
}
return Optional.of(new LatestVersion<>(latestVersionList.get(0),
latestVersionList.subList(1, latestVersionList.size())));
}
//~--- inner classes -------------------------------------------------------
/**
* The Class LatestStampAccumulator.
*/
private class LatestStampAccumulator
implements ObjIntConsumer<StampSequenceSet> {
/**
* Accept.
*
* @param stampsForPosition the stamps for position
* @param stampToCompare the stamp to compare
*/
@Override
public void accept(StampSequenceSet stampsForPosition, int stampToCompare) {
handleStamp(stampsForPosition, stampToCompare);
}
}
/**
* The Class LatestStampCombiner.
*/
private class LatestStampCombiner
implements BiConsumer<StampSequenceSet, StampSequenceSet> {
/**
* Accept.
*
* @param t the t
* @param u the u
*/
@Override
public void accept(StampSequenceSet t, StampSequenceSet u) {
u.stream().forEach((stampToTest) -> {
handleStamp(t, stampToTest);
});
u.clear();
// can't find good documentation that specifies behaviour of BiConsumer
// in this context, so am making sure both sets have the same values.
t.or(u);
}
}
/**
* The Class Segment.
*/
private class Segment {
/**
* Each segment gets it's own sequence which gets greater the further
* prior to the position of the relative position computer.
* TODO if we have a path sequence, may not need segment sequence.
*/
int segmentSequence;
/**
* The pathConceptSequence of this segment. Each ancestor path to the
* position of the computer gets it's own segment.
*/
int pathConceptSequence;
/**
* The end time of the position of the relative position computer. stamps
* with times after the end time are not part of the path.
*/
long endTime;
/** The preceding segments. */
RoaringBitmap precedingSegments;
//~--- constructors -----------------------------------------------------
/**
* Instantiates a new segment.
*
* @param segmentSequence the segment sequence
* @param pathConceptSequence the path concept sequence
* @param endTime the end time
* @param precedingSegments the preceding segments
*/
private Segment(int segmentSequence, int pathConceptSequence, long endTime, RoaringBitmap precedingSegments) {
this.segmentSequence = segmentSequence;
this.pathConceptSequence = pathConceptSequence;
this.endTime = endTime;
this.precedingSegments = new RoaringBitmap();
this.precedingSegments.or(precedingSegments);
}
//~--- methods ----------------------------------------------------------
/**
* To string.
*
* @return the string
*/
@Override
public String toString() {
return "Segment{" + this.segmentSequence + ", pathConcept=" +
Get.conceptDescriptionText(this.pathConceptSequence) + "<" + this.pathConceptSequence + ">, endTime=" +
Instant.ofEpochMilli(this.endTime) + ", precedingSegments=" + this.precedingSegments + '}';
}
/**
* Contains position.
*
* @param pathConceptSequence the path concept sequence
* @param moduleConceptSequence the module concept sequence
* @param time the time
* @return true, if successful
*/
private boolean containsPosition(int pathConceptSequence, int moduleConceptSequence, long time) {
if (RelativePositionCalculator.this.coordinate.getModuleSequences().isEmpty() ||
RelativePositionCalculator.this.coordinate.getModuleSequences().contains(moduleConceptSequence)) {
if ((this.pathConceptSequence == pathConceptSequence) && (time != Long.MIN_VALUE)) {
return time <= this.endTime;
}
}
return false;
}
}
}