diff --git a/core/annotation/src/main/java/datawave/annotation/data/v1/AnnotationDataAccess.java b/core/annotation/src/main/java/datawave/annotation/data/v1/AnnotationDataAccess.java index 1ddd8ef4e91..dcfe6827f5a 100644 --- a/core/annotation/src/main/java/datawave/annotation/data/v1/AnnotationDataAccess.java +++ b/core/annotation/src/main/java/datawave/annotation/data/v1/AnnotationDataAccess.java @@ -42,8 +42,14 @@ import datawave.annotation.util.v1.AnnotationUtils; import datawave.security.util.ScannerHelper; -/** Used to read and write annotation data to Accumulo */ -public class AnnotationDataAccess { +/** + * Accumulo-backed reader and writer for annotation data. + *

+ * Annotations are addressed by document shard. The annotation table stores document-scoped annotations under column families composed of {@code datatype}, + * {@code uid}, and annotation type, separated by {@link #NULL}. Column qualifiers begin with the annotation id, which allows scans to target either a known + * annotation type or all types for a document. Annotation sources are stored separately and addressed by analytic hash. + */ +public class AnnotationDataAccess implements AnnotationReader, AnnotationWriter, AnnotationSourceReader, AnnotationSourceWriter { public static final char NULL = '\u0000'; public static final char MAX = '\uFFFF'; @@ -57,23 +63,28 @@ public class AnnotationDataAccess { final AnnotationSerializer>,Annotation> annotationSerializer; final AnnotationSerializer>,AnnotationSource> annotationSourceSerializer; - // TODO: feature flags for now. + // TODO: feature flags for now, ultimately these might get externalized in a configuration object. boolean blockAnnotationSourceOverwrites = false; boolean blockAnnotationOverwrites = false; /** - * Create the annotation data access object + * Creates an Accumulo annotation data access object. + *

+ * The supplied serializers define the physical key/value representation used by all read and write operations. The same authorizations are used for every + * scanner created by this instance. * * @param accumuloClient - * the accumulo client to use + * the Accumulo client used to create scanners and batch writers * @param authorizations - * the authorizations used for these operations + * the authorizations used when scanning annotation data * @param annotationTableName - * the accumulo annotation table to use + * the Accumulo table that stores annotations * @param annotationSourceTableName - * the accumulo annotation source table to use + * the Accumulo table that stores annotation sources * @param annotationSerializer - * the serializer to transform protobuf to accumulo entries + * the serializer that transforms annotations to and from Accumulo entries + * @param annotationSourceSerializer + * the serializer that transforms annotation sources to and from Accumulo entries */ public AnnotationDataAccess(AccumuloClient accumuloClient, Set authorizations, String annotationTableName, String annotationSourceTableName, AnnotationSerializer>,Annotation> annotationSerializer, @@ -86,22 +97,56 @@ public AnnotationDataAccess(AccumuloClient accumuloClient, Set a this.annotationSourceSerializer = annotationSourceSerializer; } + /** + * Indicates whether this instance rejects writes for annotations whose generated id already exists. + * + * @return {@code true} when annotation id conflicts cause writes to fail + */ public boolean isBlockAnnotationOverwrites() { return blockAnnotationOverwrites; } + /** + * Configures whether this instance rejects writes for annotations whose generated id already exists. + * + * @param blockAnnotationOverwrites + * {@code true} to reject annotation id conflicts, or {@code false} to allow overwrites + */ public void setBlockAnnotationOverwrites(boolean blockAnnotationOverwrites) { this.blockAnnotationOverwrites = blockAnnotationOverwrites; } + /** + * Indicates whether this instance rejects writes for annotation sources whose generated analytic hash already exists. + * + * @return {@code true} when annotation source id conflicts cause writes to fail + */ public boolean isBlockAnnotationSourceOverwrites() { return blockAnnotationSourceOverwrites; } + /** + * Configures whether this instance rejects writes for annotation sources whose generated analytic hash already exists. + * + * @param blockAnnotationSourceOverwrites + * {@code true} to reject annotation source id conflicts, or {@code false} to allow overwrites + */ public void setBlockAnnotationSourceOverwrites(boolean blockAnnotationSourceOverwrites) { this.blockAnnotationSourceOverwrites = blockAnnotationSourceOverwrites; } + /** + * Reads an annotation source from the annotation source table using an exact row range on the analytic hash. + *

+ * The source serializer consumes all entries in the row and returns {@code null} when the row contains no visible source data. + * + * @param analyticHash + * the analytic hash row to scan + * @return the deserialized annotation source, or {@link Optional#empty()} when no source is found + * @throws AnnotationReadException + * if the source table is missing or the entries cannot be deserialized + */ + @Override public Optional getAnnotationSource(String analyticHash) { try (Scanner scanner = ScannerHelper.createScanner(accumuloClient, annotationSourceTableName, authorizations)) { final Range range = new Range(analyticHash); @@ -116,20 +161,26 @@ public Optional getAnnotationSource(String analyticHash) { } /** - * Get a specific annotation by id and type + * Reads a single annotation when the annotation type and id are both known. + *

+ * This method narrows the scan to a single shard, the exact annotation column family ({@code datatype\0uid\0annotationType}), and the column qualifier + * range for {@code annotationUid}. A {@link RegExFilter} is also applied so only entries for the requested annotation id are deserialized. * * @param shard - * the shard for the document related to this annotation. + * the shard for the annotated document * @param datatype - * the datatype of the document related to this annotation. + * the datatype for the annotated document * @param uid - * the document id related to this annotation + * the unique id for the annotated document * @param annotationType - * the type of annotation we're seeing. + * the annotation type to search * @param annotationUid - * the annotation id of the annotation we want to fetch. - * @return an Optional that will contain the retrieved annotation if found. + * the annotation id to retrieve + * @return the deserialized annotation, or {@link Optional#empty()} when no matching entries are visible + * @throws AnnotationReadException + * if the annotation table is missing or the entries cannot be deserialized */ + @Override public Optional getAnnotation(String shard, String datatype, String uid, String annotationType, String annotationUid) { try (Scanner scanner = ScannerHelper.createScanner(accumuloClient, annotationTableName, authorizations)) { final String columnFamily = datatype + NULL + uid + NULL + annotationType; @@ -156,18 +207,25 @@ public Optional getAnnotation(String shard, String datatype, String } /** - * Get the annotation with the specified annotationId without specifying a type + * Reads a single annotation by id without requiring the caller to know its annotation type. + *

+ * This method scans all annotation type column families for the document ({@code datatype\0uid\0*}) and filters column qualifiers that begin with + * {@code annotationId\0}. If entries deserialize into more than one annotation, the data is considered ambiguous and an {@link AnnotationReadException} is + * thrown. * * @param shard - * the shard for the document. + * the shard for the annotated document * @param datatype - * the datatype of the document. + * the datatype for the annotated document * @param uid - * the document id. + * the unique id for the annotated document * @param annotationId * the id of the annotation we want to retrieve - * @return an Optional that will contain the retrieved annotation if found. + * @return the matching annotation, or {@link Optional#empty()} when no matching entries are visible + * @throws AnnotationReadException + * if the annotation table is missing, the entries cannot be deserialized, or multiple annotations share the requested id for the document */ + @Override public Optional getAnnotation(String shard, String datatype, String uid, String annotationId) { try (Scanner scanner = ScannerHelper.createScanner(accumuloClient, annotationTableName, authorizations)) { final String columnFamily = datatype + NULL + uid + NULL; @@ -200,16 +258,22 @@ public Optional getAnnotation(String shard, String datatype, String } /** - * Get the annotation types for a document + * Reads the distinct annotation types for a document. + *

+ * The scan is bounded to the document's annotation column-family prefix ({@code datatype\0uid\0}) and extracts the last column-family component from each + * visible entry. Returned values are distinct and naturally ordered by the backing {@link TreeSet}. * * @param shard - * the shard for the document. + * the shard for the annotated document * @param datatype - * the datatype of the document. + * the datatype for the annotated document * @param uid - * the document id. - * @return a collection of annotation types we have for this document. + * the unique id for the annotated document + * @return the distinct annotation types visible for the document, never {@code null} + * @throws AnnotationReadException + * if the annotation table is missing */ + @Override public Collection getAnnotationTypes(String shard, String datatype, String uid) { try (Scanner scanner = ScannerHelper.createScanner(accumuloClient, annotationTableName, authorizations)) { final String columnFamilyPrefix = datatype + NULL + uid + NULL; @@ -233,16 +297,22 @@ public Collection getAnnotationTypes(String shard, String datatype, Stri } /** - * Get all annotations for a document + * Reads all annotations for a document across all annotation types. + *

+ * The scan is bounded to the document's annotation column-family prefix ({@code datatype\0uid\0}) and filtered to entries in the requested shard. Entries + * are grouped by annotation id before deserialization. * * @param shard - * the shard for the document. + * the shard for the annotated document * @param datatype - * the datatype of the document. + * the datatype for the annotated document * @param uid - * the document id. - * @return a collection of annotation types we have for this document. + * the unique id for the annotated document + * @return the annotations visible for the document, never {@code null} + * @throws AnnotationReadException + * if the annotation table is missing or any grouped annotation cannot be deserialized */ + @Override public List getAnnotations(String shard, String datatype, String uid) { try (Scanner scanner = ScannerHelper.createScanner(accumuloClient, annotationTableName, authorizations)) { final String columnFamilyPrefix = datatype + NULL + uid + NULL; @@ -265,18 +335,24 @@ public List getAnnotations(String shard, String datatype, String uid } /** - * Get all annotations of a specific type for a document + * Reads all annotations of a specific type for a document. + *

+ * This method scans the exact annotation column family ({@code datatype\0uid\0annotationType}) for the requested shard. Entries are grouped by annotation + * id before deserialization. * * @param shard - * the shard for the document. + * the shard for the annotated document * @param datatype - * the datatype of the document. + * the datatype for the annotated document * @param uid - * the document id. + * the unique id for the annotated document * @param annotationType - * the annotation types we're interested - * @return a list of annotations for this document of the specified type. + * the annotation type to retrieve + * @return the matching annotations visible for the document and type, never {@code null} + * @throws AnnotationReadException + * if the annotation table is missing or any grouped annotation cannot be deserialized */ + @Override public List getAnnotationsForType(String shard, String datatype, String uid, String annotationType) { try (Scanner scanner = ScannerHelper.createScanner(accumuloClient, annotationTableName, authorizations)) { final String columnFamily = datatype + NULL + uid + NULL + annotationType; @@ -299,12 +375,18 @@ public List getAnnotationsForType(String shard, String datatype, Str } /** - * Save a new annotation source + * Writes an annotation source to the annotation source table. + *

+ * Before writing, this method validates that the caller has not supplied a store-managed id, injects annotation source hashes, and checks for id conflicts + * according to {@link #isBlockAnnotationSourceOverwrites()}. The prepared source is serialized and written as a non-delete mutation. * * @param annotationSource - * the annotation to save. - * @return an Optional containing the save annotation if the save was successful. + * the annotation source to save + * @return the saved annotation source, including generated hashes + * @throws AnnotationWriteException + * if validation, serialization, table access, or mutation writing fails */ + @Override public Optional addAnnotationSource(AnnotationSource annotationSource) { AnnotationSource addedAnnotationSource = prepareAnnotationSourceForAdd(annotationSource); try (BatchWriter writer = accumuloClient.createBatchWriter(annotationSourceTableName)) { @@ -318,12 +400,19 @@ public Optional addAnnotationSource(AnnotationSource annotatio } /** - * Save a new annotation. + * Writes an annotation to the annotation table. + *

+ * Before writing, this method validates that the caller has not supplied store-managed annotation or segment ids, injects all annotation and segment + * hashes, and checks for id conflicts according to {@link #isBlockAnnotationOverwrites()}. The prepared annotation is serialized and written as a + * non-delete mutation. * * @param annotation - * the annotation to save. - * @return an Optional containing the save annotation if the save was successful. + * the annotation to save + * @return the saved annotation, including generated annotation and segment ids + * @throws AnnotationWriteException + * if validation, serialization, table access, or mutation writing fails */ + @Override public Optional addAnnotation(Annotation annotation) { Annotation addedAnnotation = prepareAnnotationForAdd(annotation); try (BatchWriter writer = accumuloClient.createBatchWriter(annotationTableName)) { @@ -337,15 +426,23 @@ public Optional addAnnotation(Annotation annotation) { } /** - * Update an annotation. This isn't a deletion of the old annotation and an add of the new annotation, instead we maintain both versions of the annotation - * and link them together via metadata. We will not perform any update unless the new data is valid and the existing annotationId exists. + * Writes a new annotation version that references an existing annotation id. + *

+ * This implementation first verifies that {@code targetAnnotationId} exists for the document identified by the replacement annotation. It then injects an + * update reference into the replacement annotation and delegates to {@link #addAnnotation(Annotation)}. The original annotation remains stored; this method + * does not overwrite or delete it. * * @param targetAnnotationId - * the identifier for the annotation we are providing an update for. + * the identifier for the existing annotation being updated * @param annotation - * the annotation that contains the updated annotation. - * @return an Optional containing the updated annotation if the update was successful. + * the replacement annotation data to save as a new version + * @return the saved update annotation, including generated annotation and segment ids + * @throws AnnotationUpdateException + * if the target annotation cannot be found or the update annotation cannot be saved + * @throws AnnotationWriteException + * if the delegated add operation fails validation, serialization, table access, or mutation writing */ + @Override public Optional updateAnnotation(String targetAnnotationId, Annotation annotation) { String shard = annotation.getShard(); String datatype = annotation.getDataType(); @@ -369,18 +466,24 @@ public Optional updateAnnotation(String targetAnnotationId, Annotati } /** - * Deletes all entries for annotation that matches the provided criteria + * Deletes all Accumulo entries for an annotation id on a document. + *

+ * This method scans all annotation type column families for the document and filters entries whose column qualifier begins with {@code annotationId\0}. The + * matching entries are converted to delete mutations and written back to the annotation table. * * @param shard - * the shard for the document. + * the shard for the annotated document * @param datatype - * the datatype of the document. + * the datatype for the annotated document * @param uid - * the document id. + * the unique id for the annotated document * @param annotationId - * the id of the annotation we want to delete. + * the annotation id to delete + * @throws AnnotationUpdateException + * if table access, serialization, or mutation writing fails */ - public void delete(String shard, String datatype, String uid, String annotationId) { + @Override + public void deleteAnnotation(String shard, String datatype, String uid, String annotationId) { try (Scanner scanner = ScannerHelper.createScanner(accumuloClient, annotationTableName, authorizations); BatchWriter writer = accumuloClient.createBatchWriter(annotationTableName)) { final String columnFamily = datatype + NULL + uid + NULL; @@ -407,72 +510,18 @@ public void delete(String shard, String datatype, String uid, String annotationI } /** - * Add a segment to an existing annotation. - * - * @param shard - * the shard for the document. - * @param datatype - * the datatype of the document. - * @param uid - * the document id. - * @param annotationId - * the id of the annotation to which we want to add the segment. - * @param segment - * the segment to add. - */ - public void addSegment(String shard, String datatype, String uid, String annotationId, Segment segment) { - // TODO: implement me - } - - /** - * Update a segment in an existing annotation. - * - * @param shard - * the shard for the document. - * @param datatype - * the datatype of the document. - * @param uid - * the document id. - * @param annotationId - * the id of the annotation to which we want to add the segment. - * @param segment - * the segment to update. - */ - public void updateSegment(String shard, String datatype, String uid, String annotationId, Segment segment) { - // TODO: implement me - } - - /** - * Deletes any segments with the id specified in an existing annotation. - * - * @param shard - * the shard for the document. - * @param datatype - * the datatype of the document. - * @param uid - * the document id. - * @param annotationId - * the id of the annotation to which we want to add the segment. - * @param segmentId - * the id of the segment to delete. - */ - public void deleteSegment(String shard, String datatype, String uid, String annotationId, String segmentId) { - // TODO: implement me - } - - /** - * prepare the annotation source for the addition to the datastore by performing the following: + * Prepares an annotation source for insertion into Accumulo. *

    - *
  1. validate
  2. - *
  3. assign identifiers
  4. - *
  5. check for conflicts
  6. + *
  7. validate that caller-managed data does not include store-managed ids
  8. + *
  9. assign deterministic annotation source hashes
  10. + *
  11. check the generated analytic hash for conflicts
  12. *
* * @param annotationSource - * the annotation to prepare for addition - * @return the prepared annotation/segments that should be written. + * the annotation source to prepare + * @return the annotation source that should be serialized and written * @throws AnnotationWriteException - * if there's an issue encountered when preparing and the annotation should not be written as a result. + * if validation or conflict checks fail */ protected AnnotationSource prepareAnnotationSourceForAdd(AnnotationSource annotationSource) { validateAnnotationSourceForAdd(annotationSource); @@ -482,13 +531,15 @@ protected AnnotationSource prepareAnnotationSourceForAdd(AnnotationSource annota } /** - * Validate the basic structure of the provided annotation source. This will error if the datastructures already have an id assigned because we don't want - * folks to assume they can assign their own ids to new annotations, that is the job of the data access object. + * Validates source data before this implementation assigns annotation source hashes. + *

+ * Callers are not allowed to provide the analytic hash for a new source because the hash is a store-managed identifier derived by + * {@link AnnotationUtils#injectAnnotationSourceHashes(AnnotationSource)}. * * @param annotationSource - * the annotation to validate + * the annotation source to validate * @throws AnnotationWriteException - * if there's a validation failure. + * if the annotation source already has an analytic hash */ protected void validateAnnotationSourceForAdd(AnnotationSource annotationSource) { if (StringUtils.isNotBlank(annotationSource.getAnalyticHash())) { @@ -498,16 +549,16 @@ protected void validateAnnotationSourceForAdd(AnnotationSource annotationSource) } /** - * Check the annotation source for conflicts. if we allow this annotation to be written. + * Checks whether a prepared annotation source can be written. *

    - *
  1. source id is assigned
  2. - *
  3. optional: no annotation source exists with the same id
  4. + *
  5. the generated analytic hash must be present
  6. + *
  7. when overwrite blocking is enabled, no visible annotation source may already exist for the same analytic hash
  8. *
* * @param annotationSource - * the annotation to check + * the prepared annotation source to check * @throws AnnotationWriteException - * if a check fails. + * if the analytic hash is missing or a blocked conflict is found */ protected void checkAnnotationSourceForConflicts(AnnotationSource annotationSource) { // check that the annotation has an id assigned. @@ -528,18 +579,18 @@ protected void checkAnnotationSourceForConflicts(AnnotationSource annotationSour } /** - * prepare the annotation for the addition to the datastore by performing the following: + * Prepares an annotation for insertion into Accumulo. *
    - *
  1. validate
  2. - *
  3. assign identifiers
  4. - *
  5. check for conflicts
  6. + *
  7. validate that caller-managed data does not include store-managed ids
  8. + *
  9. assign deterministic annotation and segment hashes
  10. + *
  11. check the generated annotation and segment ids for conflicts
  12. *
* * @param annotation - * the annotation to prepare for addition - * @return the prepared annotation/segments that should be written. + * the annotation to prepare + * @return the annotation that should be serialized and written * @throws AnnotationWriteException - * if there's an issue encountered when preparing and the annotation should not be written as a result. + * if validation or conflict checks fail */ protected Annotation prepareAnnotationForAdd(Annotation annotation) { validateAnnotationForAdd(annotation); @@ -549,13 +600,15 @@ protected Annotation prepareAnnotationForAdd(Annotation annotation) { } /** - * Validate the basic structure of the provided annotation. This will error if the datastructures already have an id assigned because we don't want folks to - * assume they can assign their own ids to new annotations, that is the job of the data access object. + * Validates annotation data before this implementation assigns annotation and segment hashes. + *

+ * Callers are not allowed to provide annotation or segment ids for a new annotation because those are store-managed identifiers derived by + * {@link AnnotationUtils#injectAllHashes(Annotation)}. * * @param annotation * the annotation to validate * @throws AnnotationWriteException - * if there's a validation failure. + * if the annotation already has an annotation id or any segment already has a segment hash */ protected void validateAnnotationForAdd(Annotation annotation) { if (StringUtils.isNotBlank(annotation.getAnnotationId())) { @@ -570,17 +623,18 @@ protected void validateAnnotationForAdd(Annotation annotation) { } /** - * Check the annotation for conflicts + * Checks whether a prepared annotation can be written. *

    - *
  1. annotation id is assigned
  2. - *
  3. all segment ids are assigned and there are no duplicates
  4. - *
  5. optional: no annotation exists with the same id if overwrites are disabled
  6. + *
  7. the generated annotation id must be present
  8. + *
  9. every segment must have a generated id
  10. + *
  11. segment ids must be unique within the annotation
  12. + *
  13. when overwrite blocking is enabled, no visible annotation may already exist for the same document and annotation id
  14. *
* * @param annotation - * the annotation to check + * the prepared annotation to check * @throws AnnotationWriteException - * if a check fails. + * if ids are missing, duplicate segment ids are found, or a blocked annotation conflict is found */ protected void checkAnnotationForConflicts(Annotation annotation) { // check that the annotation has an id assigned. @@ -611,11 +665,14 @@ protected void checkAnnotationForConflicts(Annotation annotation) { } /** - * The annotation type is always stored in the last slot of the column family, extract all the types found in the iterator to a set. + * Extracts distinct annotation types from Accumulo entries. + *

+ * Annotation column families are encoded as {@code datatype\0uid\0annotationType}; this method returns the component after the last {@link #NULL} delimiter + * for each entry. Results are returned in natural order. * * @param it - * the iterator to process for input. - * @return a list of distinct annotation types. + * the Accumulo entry iterator to inspect + * @return distinct annotation types found in the iterator, never {@code null} */ public static Collection extractTypesFromIterator(Iterator> it) { final Set annotationTypes = new TreeSet<>(); @@ -630,13 +687,16 @@ public static Collection extractTypesFromIterator(Iterator + * This method expects entries to be ordered so all key/value pairs for a given annotation id are contiguous. The annotation id is read from the first + * component of the column qualifier before the first {@link #NULL} delimiter. Each completed group is passed to the configured annotation serializer. * * @param it - * the iterator to process for input - * @return a list of zero to many Annotations extracted from the iterator. Never null. + * the Accumulo entry iterator to process + * @return annotations deserialized from the iterator, never {@code null} * @throws AnnotationSerializationException - * if there is a problem deserializing any portions of the Accumulo entries. + * if any grouped annotation entries cannot be deserialized */ public List extractAnnotationsFromIterator(Iterator> it) throws AnnotationSerializationException { final List> buffer = new ArrayList<>(); diff --git a/core/annotation/src/main/java/datawave/annotation/data/v1/AnnotationReader.java b/core/annotation/src/main/java/datawave/annotation/data/v1/AnnotationReader.java new file mode 100644 index 00000000000..ba6a3bd6e2f --- /dev/null +++ b/core/annotation/src/main/java/datawave/annotation/data/v1/AnnotationReader.java @@ -0,0 +1,89 @@ +package datawave.annotation.data.v1; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import datawave.annotation.protobuf.v1.Annotation; + +/** + * Read-only contract for annotation data stores. + *

+ * Implementations retrieve annotations by document identity ({@code shard}, {@code datatype}, and {@code uid}), annotation type, annotation id, and annotation + * source id. Methods return empty results when no matching data is visible to the implementation. + */ +public interface AnnotationReader { + + /** + * Retrieves a single annotation for a document when both the annotation type and annotation id are known. + * + * @param shard + * the shard for the annotated document + * @param datatype + * the datatype for the annotated document + * @param uid + * the unique id for the annotated document + * @param annotationType + * the annotation type to search + * @param annotationUid + * the annotation id to retrieve + * @return the matching annotation, or {@link Optional#empty()} when it is not found + */ + Optional getAnnotation(String shard, String datatype, String uid, String annotationType, String annotationUid); + + /** + * Retrieves a single annotation for a document by annotation id without requiring the caller to know the annotation type. + * + * @param shard + * the shard for the annotated document + * @param datatype + * the datatype for the annotated document + * @param uid + * the unique id for the annotated document + * @param annotationId + * the annotation id to retrieve + * @return the matching annotation, or {@link Optional#empty()} when it is not found + */ + Optional getAnnotation(String shard, String datatype, String uid, String annotationId); + + /** + * Lists the distinct annotation types currently available for a document. + * + * @param shard + * the shard for the annotated document + * @param datatype + * the datatype for the annotated document + * @param uid + * the unique id for the annotated document + * @return the distinct annotation types visible for the document, never {@code null} + */ + Collection getAnnotationTypes(String shard, String datatype, String uid); + + /** + * Retrieves all annotations currently available for a document. + * + * @param shard + * the shard for the annotated document + * @param datatype + * the datatype for the annotated document + * @param uid + * the unique id for the annotated document + * @return the annotations visible for the document, never {@code null} + */ + List getAnnotations(String shard, String datatype, String uid); + + /** + * Retrieves all annotations of a specific type for a document. + * + * @param shard + * the shard for the annotated document + * @param datatype + * the datatype for the annotated document + * @param uid + * the unique id for the annotated document + * @param annotationType + * the annotation type to retrieve + * @return the matching annotations visible for the document and type, never {@code null} + */ + List getAnnotationsForType(String shard, String datatype, String uid, String annotationType); +} diff --git a/core/annotation/src/main/java/datawave/annotation/data/v1/AnnotationSourceReader.java b/core/annotation/src/main/java/datawave/annotation/data/v1/AnnotationSourceReader.java new file mode 100644 index 00000000000..03767aefa83 --- /dev/null +++ b/core/annotation/src/main/java/datawave/annotation/data/v1/AnnotationSourceReader.java @@ -0,0 +1,22 @@ +package datawave.annotation.data.v1; + +import java.util.Optional; + +import datawave.annotation.protobuf.v1.AnnotationSource; + +/** + * Read-only contract for annotation source data stores. + *

+ * Implementations retrieve annotation sources by annotation source id. Methods return empty results when no matching data is visible to the implementation. + */ +public interface AnnotationSourceReader { + + /** + * Retrieves the annotation source identified by its analytic hash. + * + * @param analyticHash + * the analytic hash assigned to the source + * @return the matching annotation source, or {@link Optional#empty()} when it is not found + */ + Optional getAnnotationSource(String analyticHash); +} diff --git a/core/annotation/src/main/java/datawave/annotation/data/v1/AnnotationSourceWriter.java b/core/annotation/src/main/java/datawave/annotation/data/v1/AnnotationSourceWriter.java new file mode 100644 index 00000000000..cfbfd89f2c5 --- /dev/null +++ b/core/annotation/src/main/java/datawave/annotation/data/v1/AnnotationSourceWriter.java @@ -0,0 +1,24 @@ +package datawave.annotation.data.v1; + +import java.util.Optional; + +import datawave.annotation.protobuf.v1.AnnotationSource; + +/** + * Write contract for annotation source data stores. + *

+ * Implementations are responsible for validating writable annotation data, assigning store-managed identifiers when needed, and persisting annotation source + * changes. + */ +public interface AnnotationSourceWriter { + + /** + * Adds a new annotation source. + * + * @param annotationSource + * the annotation source to add; callers should not pre-populate store-managed ids + * @return the persisted annotation source, including any ids assigned by the writer + */ + Optional addAnnotationSource(AnnotationSource annotationSource); + +} diff --git a/core/annotation/src/main/java/datawave/annotation/data/v1/AnnotationWriter.java b/core/annotation/src/main/java/datawave/annotation/data/v1/AnnotationWriter.java new file mode 100644 index 00000000000..37ae718d86b --- /dev/null +++ b/core/annotation/src/main/java/datawave/annotation/data/v1/AnnotationWriter.java @@ -0,0 +1,50 @@ +package datawave.annotation.data.v1; + +import java.util.Optional; + +import datawave.annotation.protobuf.v1.Annotation; + +/** + * Write contract for annotation data stores. + *

+ * Implementations are responsible for validating writable annotation data, assigning store-managed identifiers when needed, and persisting annotation changes. + */ +public interface AnnotationWriter { + + /** + * Adds a new annotation. + * + * @param annotation + * the annotation to add; callers should not pre-populate store-managed annotation or segment ids + * @return the persisted annotation, including any ids assigned by the writer + */ + Optional addAnnotation(Annotation annotation); + + /** + * Creates an update for an existing annotation. + *

+ * Implementations may preserve previous annotation versions and link the new annotation back to {@code targetAnnotationId} instead of overwriting the + * existing annotation in place. + * + * @param targetAnnotationId + * the id of the existing annotation being updated + * @param annotation + * the updated annotation data to persist + * @return the persisted update annotation, including any ids assigned by the writer + */ + Optional updateAnnotation(String targetAnnotationId, Annotation annotation); + + /** + * Deletes all stored entries for a document annotation id. + * + * @param shard + * the shard for the annotated document + * @param datatype + * the datatype for the annotated document + * @param uid + * the unique id for the annotated document + * @param annotationId + * the annotation id to delete + */ + void deleteAnnotation(String shard, String datatype, String uid, String annotationId); +} diff --git a/core/annotation/src/test/java/datawave/annotation/data/v1/AnnotationDataAccessTest.java b/core/annotation/src/test/java/datawave/annotation/data/v1/AnnotationDataAccessTest.java index 8ded425748c..1deb7d18b33 100644 --- a/core/annotation/src/test/java/datawave/annotation/data/v1/AnnotationDataAccessTest.java +++ b/core/annotation/src/test/java/datawave/annotation/data/v1/AnnotationDataAccessTest.java @@ -383,7 +383,7 @@ public void testDeleteAnnotation() { assertTrue(existingAnnotation.isPresent()); // Delete the annotation - dao.delete(shard, dataType, uid, annotationIdToDelete); + dao.deleteAnnotation(shard, dataType, uid, annotationIdToDelete); // Verify the annotation no longer exists Optional deletedAnnotation = dao.getAnnotation(shard, dataType, uid, annotationIdToDelete); @@ -401,7 +401,7 @@ public void testDeleteAnnotationNotFound() { // This should not throw an exception (delete handles empty iterator gracefully) // Note: if delete returns null mutation, writer.addMutation is called with null which throws try { - dao.delete(shard, dataType, uid, annotationId); + dao.deleteAnnotation(shard, dataType, uid, annotationId); // If we get here, delete handled empty results correctly } catch (IllegalArgumentException e) { // This is expected if mutationAdapter returns null and writer.addMutation is called with null