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
+ * 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
+ * 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
+ * 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
+ * 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
+ * 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
+ * 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
+ * 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
+ * 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
+ * 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
+ * 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
+ * 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.
*
+ * 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.
*
+ * 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.
*
+ * 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
+ * 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
+ * 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
+ * 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
+ * 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
+ * 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
- *
*
* @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.
+ *
- *
*
* @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.
*
- *
*
* @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.
+ *
- *
*
* @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.
+ *