All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dev.ikm.tinkar.provider.entity.EntityProvider Maven / Gradle / Ivy

/*
 * Copyright © 2015 Integrated Knowledge Management ([email protected])
 *
 * 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.
 */
package dev.ikm.tinkar.provider.entity;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.auto.service.AutoService;
import dev.ikm.tinkar.common.alert.AlertObject;
import dev.ikm.tinkar.common.alert.AlertStreams;
import dev.ikm.tinkar.common.id.PublicId;
import dev.ikm.tinkar.common.service.*;
import dev.ikm.tinkar.common.util.broadcast.Broadcaster;
import dev.ikm.tinkar.common.util.broadcast.SimpleBroadcaster;
import dev.ikm.tinkar.common.util.broadcast.Subscriber;
import dev.ikm.tinkar.common.util.uuid.UuidUtil;
import dev.ikm.tinkar.component.Chronology;
import dev.ikm.tinkar.component.Stamp;
import dev.ikm.tinkar.component.Version;
import dev.ikm.tinkar.entity.*;
import dev.ikm.tinkar.entity.transaction.Transaction;
import dev.ikm.tinkar.terms.EntityFacade;
import dev.ikm.tinkar.terms.State;
import dev.ikm.tinkar.terms.TinkarTerm;
import org.eclipse.collections.api.factory.Sets;
import org.eclipse.collections.api.factory.primitive.IntSets;
import org.eclipse.collections.api.list.ImmutableList;
import org.eclipse.collections.api.set.MutableSet;
import org.eclipse.collections.api.set.primitive.ImmutableIntSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Arrays;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.function.Consumer;

import static dev.ikm.tinkar.terms.TinkarTerm.DESCRIPTION_PATTERN;

//@AutoService({EntityService.class, PublicIdService.class, DefaultDescriptionForNidService.class})
public class EntityProvider implements EntityService, PublicIdService, DefaultDescriptionForNidService, EntityDataRepair {

    private static final Logger LOG = LoggerFactory.getLogger(EntityProvider.class);
    private static final Cache STRING_CACHE = Caffeine.newBuilder().maximumSize(1024).build();
    private static final Cache ENTITY_CACHE = Caffeine.newBuilder().maximumSize(10240).build();
    private static final Cache STAMP_CACHE = Caffeine.newBuilder().maximumSize(1024).build();


    //Multi> chronologyBroadcaster = BroadcastProcessor.create().toHotStream();
    //  >
    final Broadcaster processor;

    /**
     * TODO elegant shutdown of entityStream and others
     */
    public EntityProvider() {
        LOG.info("Constructing EntityProvider");
        this.processor = new SimpleBroadcaster<>();
        // Ensure that the non-existent stamp is always available.
        // Write is idempotent, so writing each time should not cause any problems.
        this.putEntity(StampRecord.nonExistentStamp());
    }

    public void addSubscriberWithWeakReference(Subscriber subscriber) {
        this.processor.addSubscriberWithWeakReference(subscriber);
    }

    @Override
    public String textFast(int nid) {

        // TODO use a default language coordinate instead of this hardcode routine.
        return STRING_CACHE.get(nid, integer -> {
            int[] semanticNids = PrimitiveData.get().semanticNidsForComponentOfPattern(nid, DESCRIPTION_PATTERN.nid());
            String anyString = null;
            String fqnString = null;
            for (int semanticNid : semanticNids) {
                Entity descriptionSemanticEntity = Entity.getFast(semanticNid);
                if (descriptionSemanticEntity instanceof SemanticEntity descriptionSemantic) {
                    Entity entity = Entity.getFast(descriptionSemantic.patternNid());
                    if (entity instanceof PatternEntity pattern) {
                        // TODO: use version computer to get version
                        PatternEntityVersion patternEntityVersion = (PatternEntityVersion) pattern.versions().get(0);
                        SemanticEntityVersion version = (SemanticEntityVersion) descriptionSemantic.versions().get(0);
                        int indexForMeaning = patternEntityVersion.indexForMeaning(TinkarTerm.DESCRIPTION_TYPE);
                        int indexForText = patternEntityVersion.indexForMeaning(TinkarTerm.TEXT_FOR_DESCRIPTION);
                        if (version.fieldValues().get(indexForMeaning).equals(TinkarTerm.REGULAR_NAME_DESCRIPTION_TYPE)) {
                            return (String) version.fieldValues().get(indexForText);
                        }
                        if (version.fieldValues().get(indexForMeaning).equals(TinkarTerm.FULLY_QUALIFIED_NAME_DESCRIPTION_TYPE)) {
                            fqnString = (String) version.fieldValues().get(indexForText);
                        }
                        anyString = (String) version.fieldValues().get(indexForText);
                    } else {
                        anyString = " <" + entity.nid() + ">" + entity.asUuidList().toString();
                        // Added in case entity.toString() itself throws an exception, at least get a UUID for the problem.
                        AlertStreams.getRoot().dispatch(AlertObject.makeError(new IllegalStateException("Expecting a pattern entity. Found entity with id:  " + anyString)));
                        AlertStreams.getRoot().dispatch(AlertObject.makeError(new IllegalStateException("Expecting a pattern entity. Found: " + entity)));
                    }
                } else {
                    anyString = " <" + descriptionSemanticEntity.nid() + "> " + descriptionSemanticEntity.asUuidList().toString();
                    LOG.error("ERROR getting string for nid: " + anyString);
                    LOG.error("ERROR Nid - 2: <" + (nid - 2) + "> " + getChronology(nid - 2));
                    LOG.error("ERROR Nid - 1: <" + (nid - 1) + "> " + getChronology(nid - 1));
                    LOG.error("ERROR Nid: <" + nid + "> " + getChronology(nid - 1));
                    LOG.error("ERROR Nid + 1: <" + (nid + 1) + "> " + getChronology(nid + 1));
                    LOG.error("ERROR Nid + 2: <" + (nid + 2) + "> " + getChronology(nid + 2));

                    // Added in case entity.toString() itself throws an exception, at least get a UUID for the problem.
                    AlertStreams.getRoot().dispatch(AlertObject.makeError(new IllegalStateException("Expecting a description semantic entity from list: " +
                            Arrays.toString(semanticNids) + "\n Found entity with id:  " + anyString)));
                    AlertStreams.getRoot().dispatch(AlertObject.makeError(new IllegalStateException("Expecting a description semantic. Found: " + descriptionSemanticEntity)));
                }
            }
            if (fqnString != null) {
                return fqnString;
            }
            return anyString;
        });

    }

    @Override
    public , V extends Version> Optional getChronology(int nid) {
        Entity entity = getEntityFast(nid);
        if (entity == null || entity.canceled()) {
            return Optional.empty();
        }
        return Optional.of((T) entity);
    }

    @Override
    public int nidForUuids(UUID... uuids) {
        return PrimitiveData.get().nidForUuids(uuids);
    }

    @Override
    public int nidForPublicId(PublicId publicId) {
        return PrimitiveData.get().nidForUuids(publicId.asUuidArray());
    }

    public , V extends EntityVersion> T getEntityFast(int nid) {
        return (T) ENTITY_CACHE.get(nid, entityNid -> {
            byte[] bytes = PrimitiveData.get().getBytes(nid);
            if (bytes == null) {
                return null;
            }
            return EntityRecordFactory.make(bytes);
        });
    }

    @Override
    public int nidForUuids(ImmutableList uuidList) {
        return PrimitiveData.get().nidForUuids(uuidList);
    }

    @Override
    public StampEntity getStampFast(int nid) {
        return STAMP_CACHE.get(nid, stampNid -> {
                    byte[] bytes = PrimitiveData.get().getBytes(nid);
                    if (bytes == null) {
                        return null;
                    }
                    return EntityRecordFactory.make(bytes);
                }
        );
    }

    @Override
    public void putEntity(Entity entity) {
        invalidateCaches(entity);
        if (entity instanceof StampEntity stampEntity) {
            STAMP_CACHE.put(stampEntity.nid(), stampEntity);
            if (stampEntity.lastVersion().stateNid() == State.CANCELED.nid()) {
                PrimitiveData.get().addCanceledStampNid(stampEntity.nid());
            }
        }
        byte[] mergedEntityBytes;
        if (entity instanceof SemanticEntity semanticEntity) {
            mergedEntityBytes = PrimitiveData.get().merge(entity.nid(),
                    semanticEntity.patternNid(),
                    semanticEntity.referencedComponentNid(),
                    entity.getBytes(), entity);
        } else {
            mergedEntityBytes = PrimitiveData.get().merge(entity.nid(), Integer.MAX_VALUE, Integer.MAX_VALUE, entity.getBytes(), entity);
        }
        ENTITY_CACHE.put(entity.nid(),  EntityRecordFactory.make(mergedEntityBytes));
        processor.dispatch(entity.nid());
        if (entity instanceof SemanticEntity semanticEntity) {
            processor.dispatch(semanticEntity.referencedComponentNid());
        }
    }

    @Override
    public void putEntityQuietly(Entity entity) {
        invalidateCaches(entity);
        if (entity instanceof StampEntity stampEntity) {
            STAMP_CACHE.put(stampEntity.nid(), stampEntity);
            if (stampEntity.lastVersion().stateNid() == State.CANCELED.nid()) {
                PrimitiveData.get().addCanceledStampNid(stampEntity.nid());
            }
        }
        byte[] mergedEntityBytes;
        if (entity instanceof SemanticEntity semanticEntity) {
            mergedEntityBytes = PrimitiveData.get().merge(entity.nid(),
                    semanticEntity.patternNid(),
                    semanticEntity.referencedComponentNid(),
                    entity.getBytes(), entity);
        } else {
            mergedEntityBytes = PrimitiveData.get().merge(entity.nid(), Integer.MAX_VALUE, Integer.MAX_VALUE, entity.getBytes(), entity);
        }
        ENTITY_CACHE.put(entity.nid(),  EntityRecordFactory.make(mergedEntityBytes));
    }

    @Override
    public void putStamp(StampEntity stampEntity) {
        putEntity(stampEntity);
    }

    @Override
    public void invalidateCaches(Entity entity) {
        invalidateCaches(entity.nid());
        if (entity instanceof SemanticEntity semanticEntity) {
            invalidateCaches(semanticEntity.referencedComponentNid(), semanticEntity.patternNid());
            Entity parent = getEntityFast(semanticEntity.referencedComponentNid());
            while (parent != null) {
                switch (parent) {
                    case ConceptEntity conceptEntity -> {
                        parent = null;
                        STRING_CACHE.invalidate(conceptEntity.nid());
                    }
                    case PatternEntity patternEntity -> {
                        parent = null;
                        STRING_CACHE.invalidate(patternEntity.nid());
                    }
                    case SemanticEntity semantic -> {
                        // If semantic is a dialect, might invalidate preferred description,
                        // so need to go up to concept or pattern to invalidate strings in cache.
                        parent = getEntityFast(semantic.referencedComponentNid());
                        STRING_CACHE.invalidate(semantic.nid());
                    }
                    default -> throw new IllegalStateException("Unexpected value: " + parent);
                }
            }
        }
    }

    @Override
    public void invalidateCaches(int... nids) {
        for (int nid : nids) {
            STRING_CACHE.invalidate(nid);
            ENTITY_CACHE.invalidate(nid);
            STAMP_CACHE.invalidate(nid);
        }
    }

    @Override
    public Entity unmarshalChronology(byte[] bytes) {
        return EntityRecordFactory.make(bytes);
    }

    @Override
    public void forEachSemanticOfPattern(int patternNid, Consumer> procedure) {
        PrimitiveData.get().forEachSemanticNidOfPattern(patternNid, (int nid) -> procedure.accept(getEntityFast(nid)));
    }

    @Override
    public int[] semanticNidsOfPattern(int patternNid) {
        return PrimitiveData.get().semanticNidsOfPattern(patternNid);
    }

    @Override
    public void forEachSemanticForComponent(int componentNid, Consumer> procedure) {
        PrimitiveData.get().forEachSemanticNidForComponent(componentNid, (int nid) -> procedure.accept(getEntityFast(nid)));
    }

    @Override
    public int[] semanticNidsForComponent(int componentNid) {
        return PrimitiveData.get().semanticNidsForComponent(componentNid);
    }

    @Override
    public void forEachSemanticForComponentOfPattern(int componentNid, int patternNid, Consumer> procedure) {
        PrimitiveData.get().forEachSemanticNidForComponentOfPattern(componentNid, patternNid, (int nid) -> procedure.accept(getEntityFast(nid)));
    }

    @Override
    public int[] semanticNidsForComponentOfPattern(int componentNid, int patternNid) {
        return PrimitiveData.get().semanticNidsForComponentOfPattern(componentNid, patternNid);
    }

    @Override
    public void notifyRefreshRequired(Transaction transaction) {
        transaction.forEachComponentInTransaction(nid -> {
            Entity.get(nid).ifPresent(entity -> invalidateCaches(entity));
            this.processor.dispatch(nid);
        });
    }

    @Override
    public PublicId publicId(int nid) {
        return getEntityFast(nid).publicId();
    }

    @Override
    public , V extends Version> Optional getChronology(PublicId publicId) {
        Entity entity;
        if (publicId instanceof EntityFacade entityFacade) {
            entity = getEntityFast(entityFacade.nid());
        } else {
            entity = getEntityFast(nidForPublicId(publicId));
        }
        if (entity == null || entity.canceled()) {
            return Optional.empty();
        }
        return Optional.of((T) entity);
    }

    //TODO-aks8m:
    // This seems to be counter intuitive when implementing protobuf transforms, or at least not straightforward when
    // implementing. Suggest we refactor to just use Entity. The use of chronology seems to not be well conveyed, but
    // only understood because I've been working with other versions of this code.
    // KEC: the use of chronology was to make it transparent to put a DTO vs an entity. If we eliminate DTOs,
    // then we can revise and potentially eliminate.
    @Override
    public , V extends Version> void putChronology(T chronology) {
        if (chronology instanceof Entity entity) {
            putEntity(entity);
        } else {
            putEntity(EntityRecordFactory.make((Chronology) chronology));
            for (Version version : chronology.versions()) {
                Stamp stamp = version.stamp();
                int nid = PrimitiveData.get().nidForUuids(stamp.publicId().asUuidArray());
                if (PrimitiveData.get().getBytes(nid) == null) {
                    putEntity(EntityRecordFactory.make(stamp));
                }
            }
        }
    }

    @AutoService(CachingService.class)
    public static class CacheProvider implements CachingService {

        @Override
        public void reset() {
            LOG.info("Resetting Entity Caches");
            STRING_CACHE.invalidateAll();
            ENTITY_CACHE.invalidateAll();
            STAMP_CACHE.invalidateAll();
        }
    }

    @Override
    public void dispatch(Integer item) {
        this.processor.dispatch(item);
    }

    @Override
    public void removeSubscriber(Subscriber subscriber) {
        this.processor.removeSubscriber(subscriber);
    }

    @Override
    public void erase(Entity entity) {
        if (PrimitiveData.get() instanceof PrimitiveDataRepair primitiveDataRepair) {
            primitiveDataRepair.erase(entity.nid());
        } else {
            throw new UnsupportedOperationException("PrimitiveDataRepair is not supported by: " +
                    PrimitiveData.get().getClass().getName());
        }
    }

    @Override
    public Future mergeThenErase(Entity entityToMergeInto, Entity entityToErase) {
        if (entityToMergeInto.getClass().equals(entityToMergeInto.getClass())) {
            if (PrimitiveData.get() instanceof PrimitiveDataRepair primitiveDataRepair) {
                FutureTask mergeThenEraseTask = new FutureTask<>(() -> {
                    Future mergedEntity = mergeEntities(entityToMergeInto, entityToErase);
                    Entity entityToKeep = mergedEntity.get();
                    erase(entityToErase);
                    primitiveDataRepair.put(entityToMergeInto.nid(), mergedEntity.get().getBytes());
                    return entityToKeep;
                });
                return (Future) TinkExecutor.threadPool().submit(mergeThenEraseTask);
            } else {
                throw new UnsupportedOperationException("PrimitiveDataRepair is not supported by: " +
                        PrimitiveData.get().getClass().getName());
            }
        } else {
            throw new IllegalStateException("Cannot merge entities of different types: \n" +
                    entityToErase + "\n\n" + entityToMergeInto);
        }
    }

    private static Future mergeEntities(Entity entityToMergeInto, Entity entityToMergeFrom) {
        // TODO Need to handle different IDs. ?
        ImmutableIntSet entityOneStampNidSet = IntSets.immutable.ofAll(entityToMergeInto.versions().stream().mapToInt(version -> version.stampNid()));
        ImmutableIntSet entityTwoStampNidSet = IntSets.immutable.ofAll(entityToMergeFrom.versions().stream().mapToInt(version -> version.stampNid()));
        ImmutableIntSet stampDifferenceNids = entityOneStampNidSet.difference(entityTwoStampNidSet);
        ImmutableIntSet stampUnionNids = entityOneStampNidSet.intersect(entityTwoStampNidSet);
        ImmutableIntSet allStampNids = stampDifferenceNids.newWithAll(stampUnionNids);

        for (int unionStamp : stampUnionNids.toArray()) {
            if (!entityToMergeInto.getVersion(unionStamp).equals(entityToMergeFrom.getVersion(unionStamp))) {
                return EntityMergeServiceFinder.adjudicatedMerge(entityToMergeInto, entityToMergeFrom);
            }
        }
        // At this point, all versions with the same stamps have equal fields.
        if (stampDifferenceNids.isEmpty()) {
            return new FutureTask<>(() -> entityToMergeInto);
        }

        // Check to see if any of the stampDifferenceNids versions have the same time, module, and path
        for (int differenceStampNid : stampDifferenceNids.toArray()) {
            StampEntity differenceStamp = EntityService.get().getStampFast(differenceStampNid);
            for (int anyStampNid : allStampNids.toArray()) {
                if (differenceStampNid != anyStampNid) {
                    StampEntity anyStamp = EntityService.get().getStampFast(anyStampNid);
                    if (differenceStamp.time() == anyStamp.time() &&
                            differenceStamp.moduleNid() == anyStamp.moduleNid() &&
                            differenceStamp.pathNid() == anyStamp.pathNid()) {
                        // Can't have 2 changes at the same virtual point of time, module, path
                        return EntityMergeServiceFinder.adjudicatedMerge(entityToMergeInto, entityToMergeFrom);
                    }
                }
            }
        }
        // At this point, all stamps represent distinct time, module, and path. We can just merge the versions.
        return switch (entityToMergeInto) {
            case ConceptRecord conceptOneRecord when entityToMergeFrom instanceof ConceptRecord conceptTwoRecord
                    -> mergeAllConceptVersions(conceptOneRecord, conceptTwoRecord);
            case PatternRecord patternOneRecord when entityToMergeFrom instanceof PatternRecord patternTwoRecord
                -> mergeAllPatternVersions(patternOneRecord, patternTwoRecord);
            case SemanticRecord semanticOneRecord when entityToMergeFrom instanceof SemanticRecord semanticTwoRecord
                    -> mergeAllSemanticVersions(semanticOneRecord, semanticTwoRecord);
            default -> throw new IllegalStateException("Can't merge:\n" + entityToMergeInto + "\nand:\n" + entityToMergeFrom);
        };
    }

    private static Future mergeAllPatternVersions(PatternRecord patternOneRecord, PatternRecord patternTwoRecord) {
        // All versions are distinct. Merge using union...
        RecordListBuilder versionList = RecordListBuilder.make();
        patternOneRecord.versions().forEach(versionRecord -> versionList.add(versionRecord));
        patternTwoRecord.versions().forEach(versionRecord -> versionList.add(versionRecord));
        MutableSet additionalUuids = Sets.mutable.ofAll(patternOneRecord.asUuidList());
        additionalUuids.addAll(patternTwoRecord.asUuidList().castToList());
        additionalUuids.remove(new UUID(patternOneRecord.mostSignificantBits(), patternOneRecord.leastSignificantBits()));
        long[] additionalUuidLongs = null;
        if (additionalUuids.notEmpty()) {
            additionalUuidLongs = UuidUtil.asArray(additionalUuids.toArray(new UUID[additionalUuids.size()] ));
        }
        PatternRecordBuilder builder = patternOneRecord.with().versions(versionList).additionalUuidLongs(additionalUuidLongs);
        return new FutureTask<>(() -> builder.build());
    }

    private static Future mergeAllSemanticVersions(SemanticRecord semanticOneRecord, SemanticRecord semanticTwoRecord) {
        // All versions are distinct. Merge using union...
        RecordListBuilder versionList = RecordListBuilder.make();
        semanticOneRecord.versions().forEach(versionRecord -> versionList.add(versionRecord));
        semanticTwoRecord.versions().forEach(versionRecord -> versionList.add(versionRecord));
        MutableSet additionalUuids = Sets.mutable.ofAll(semanticOneRecord.asUuidList());
        additionalUuids.addAll(semanticTwoRecord.asUuidList().castToList());
        additionalUuids.remove(new UUID(semanticOneRecord.mostSignificantBits(), semanticOneRecord.leastSignificantBits()));
        long[] additionalUuidLongs = null;
        if (additionalUuids.notEmpty()) {
            additionalUuidLongs = UuidUtil.asArray(additionalUuids.toArray(new UUID[additionalUuids.size()] ));
        }
       SemanticRecordBuilder builder = semanticOneRecord.with().versions(versionList).additionalUuidLongs(additionalUuidLongs);
        return new FutureTask<>(() -> builder.build());
    }

    private static FutureTask mergeAllConceptVersions(ConceptRecord conceptOneRecord, ConceptRecord conceptTwoRecord) {
        // All versions are distinct. Merge using union...
        RecordListBuilder versionList = RecordListBuilder.make();
        conceptOneRecord.versions().forEach(conceptVersionRecord -> versionList.add(conceptVersionRecord));
        conceptTwoRecord.versions().forEach(conceptVersionRecord -> versionList.add(conceptVersionRecord));
        MutableSet additionalUuids = Sets.mutable.ofAll(conceptOneRecord.asUuidList());
        additionalUuids.addAll(conceptTwoRecord.asUuidList().castToList());
        additionalUuids.remove(new UUID(conceptOneRecord.mostSignificantBits(), conceptOneRecord.leastSignificantBits()));
        long[] additionalUuidLongs = null;
        if (additionalUuids.notEmpty()) {
            additionalUuidLongs = UuidUtil.asArray(additionalUuids.toArray(new UUID[additionalUuids.size()] ));
        }
        ConceptRecordBuilder builder = conceptOneRecord.with().versions(versionList).additionalUuidLongs(additionalUuidLongs);
        return new FutureTask<>(() -> builder.build());
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy