DS-4226 Improvements to Entity validation first pass

This commit is contained in:
Andrew Wood
2019-09-09 10:20:47 -04:00
parent 74ec72add1
commit 3739b718eb
3 changed files with 425 additions and 299 deletions

View File

@@ -17,8 +17,8 @@ import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import javax.annotation.Nullable;
@@ -38,7 +38,6 @@ import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Collection;
import org.dspace.content.DSpaceObject;
import org.dspace.content.Entity;
import org.dspace.content.EntityType;
import org.dspace.content.Item;
import org.dspace.content.MetadataField;
import org.dspace.content.MetadataSchemaEnum;
@@ -106,19 +105,44 @@ public class MetadataImport {
protected static final String AC_PREFIX = "authority.controlled.";
/**
* Map of field:value to csv row number, used to resolve indirect entity references.
* Map of field:value to csv row number, used to resolve indirect entity target references.
*
* @see #populateRefAndRowMap(DSpaceCSVLine, int, UUID)
* @see #populateRefAndRowMap(DSpaceCSVLine, UUID)
*/
protected HashMap<String, Set<Integer>> csvRefMap = new HashMap<>();
protected Map<String, Set<Integer>> csvRefMap = new HashMap<>();
/**
* Map of csv row number to UUID, used to resolve indirect entity references.
* Map of csv row number to UUID, used to resolve indirect entity target references.
*
* @see #populateRefAndRowMap(DSpaceCSVLine, int, UUID)
* @see #populateRefAndRowMap(DSpaceCSVLine, UUID)
*/
protected HashMap<Integer, UUID> csvRowMap = new HashMap<>();
/**
* Map of UUIDs to their entity types.
*
* @see #populateRefAndRowMap(DSpaceCSVLine, UUID)
*/
protected static HashMap<UUID, String> entityTypeMap = new HashMap<>();
/**
* Map of UUIDs to their relations that are referenced within any import with their referers.
*
* @see #populateEntityRelationMap(String, String, String)
*/
protected static HashMap<String, HashMap<String, ArrayList<String>>> entityRelationMap = new HashMap<>();
/**
* Collection of errors generated during relation validation process.
*/
protected ArrayList<String> relationValidationErrors = new ArrayList<>();
/**
* Counter of rows proccssed in a CSV.
*/
protected Integer rowCount = 1;
/**
* Logger
*/
@@ -184,10 +208,9 @@ public class MetadataImport {
c.setMode(Context.Mode.BATCH_EDIT);
// Process each change
int rowCount = 1;
for (DSpaceCSVLine line : toImport) {
//Resolve references to other items
populateRefAndRowMap(line, rowCount, null);
// Resolve target references to other items
populateRefAndRowMap(line, line.getID());
line = resolveEntityRefs(line);
// Get the DSpace item to compare with
UUID id = line.getID();
@@ -201,7 +224,7 @@ public class MetadataImport {
WorkflowItem wfItem = null;
Item item = null;
// Is this a new item?
// Is this an existing item?
if (id != null) {
// Get the item
item = itemService.find(c, id);
@@ -238,9 +261,8 @@ public class MetadataImport {
}
}
}
// Compare
compare(item, fromCSV, change, md, whatHasChanged, line);
compareAndUpdate(item, fromCSV, change, md, whatHasChanged, line);
}
}
@@ -378,30 +400,23 @@ public class MetadataImport {
item = wsItem.getItem();
// Add the metadata to the item
List<BulkEditMetadataValue> relationships = new LinkedList<>();
for (BulkEditMetadataValue dcv : whatHasChanged.getAdds()) {
itemService.addMetadata(c, item, dcv.getSchema(),
dcv.getElement(),
dcv.getQualifier(),
dcv.getLanguage(),
dcv.getValue(),
dcv.getAuthority(),
dcv.getConfidence());
}
//Add relations after all metadata has been processed
for (BulkEditMetadataValue dcv : whatHasChanged.getAdds()) {
if (StringUtils.equals(dcv.getSchema(), MetadataSchemaEnum.RELATION.getName())) {
if (!StringUtils.equals(dcv.getElement(), "type")) {
relationships.add(dcv);
} else {
handleRelationshipMetadataValueFromBulkEditMetadataValue(item, dcv);
}
} else {
itemService.addMetadata(c, item, dcv.getSchema(),
dcv.getElement(),
dcv.getQualifier(),
dcv.getLanguage(),
dcv.getValue(),
dcv.getAuthority(),
dcv.getConfidence());
addRelationship(c, item, dcv.getElement(), dcv.getValue());
}
}
for (BulkEditMetadataValue relationship : relationships) {
handleRelationshipMetadataValueFromBulkEditMetadataValue(item, relationship);
}
// Should the workflow be used?
if (useWorkflow) {
WorkflowService workflowService = WorkflowServiceFactory.getInstance().getWorkflowService();
@@ -424,8 +439,6 @@ public class MetadataImport {
}
}
// Commit changes to the object
// c.commit();
whatHasChanged.setItem(item);
}
@@ -439,7 +452,7 @@ public class MetadataImport {
c.uncacheEntity(wfItem);
c.uncacheEntity(item);
}
populateRefAndRowMap(line, rowCount, item == null ? null : item.getID());
populateRefAndRowMap(line, item == null ? null : item.getID());
// keep track of current rows processed
rowCount++;
}
@@ -452,32 +465,14 @@ public class MetadataImport {
}
// Return the changes
if (!change ) {
validateExpressedRelations();
}
return changes;
}
/**
* This metod handles the BulkEditMetadataValue objects that correspond to Relationship metadatavalues
* @param item The item to which this metadatavalue will belong
* @param dcv The BulkEditMetadataValue to be processed
* @throws SQLException If something goes wrong
* @throws AuthorizeException If something goes wrong
*/
private void handleRelationshipMetadataValueFromBulkEditMetadataValue(Item item, BulkEditMetadataValue dcv)
throws SQLException, AuthorizeException, MetadataImportException {
LinkedList<String> values = new LinkedList<>();
values.add(dcv.getValue());
LinkedList<String> authorities = new LinkedList<>();
authorities.add(dcv.getAuthority());
LinkedList<Integer> confidences = new LinkedList<>();
confidences.add(dcv.getConfidence());
handleRelationMetadata(c, item, dcv.getSchema(), dcv.getElement(),
dcv.getQualifier(),
dcv.getLanguage(), values, authorities, confidences);
}
/**
* Compare an item metadata with a line from CSV, and optionally update the item
* Compare an item metadata with a line from CSV, and optionally update the item.
*
* @param item The current item metadata
* @param fromCSV The metadata from the CSV file
@@ -487,9 +482,10 @@ public class MetadataImport {
* @param line line in CSV file
* @throws SQLException if there is a problem accessing a Collection from the database, from its handle
* @throws AuthorizeException if there is an authorization problem with permissions
* @throws MetadataImportException custom exception for error handling within metadataimport
*/
protected void compare(Item item, String[] fromCSV, boolean change,
String md, BulkEditChange changes, DSpaceCSVLine line)
protected void compareAndUpdate(Item item, String[] fromCSV, boolean change,
String md, BulkEditChange changes, DSpaceCSVLine line)
throws SQLException, AuthorizeException, MetadataImportException {
// Log what metadata element we're looking at
String all = "";
@@ -663,10 +659,9 @@ public class MetadataImport {
}
}
if (StringUtils.equals(schema, MetadataSchemaEnum.RELATION.getName())) {
List<RelationshipType> relationshipTypeList = relationshipTypeService
.findByLeftwardOrRightwardTypeName(c, element);
.findByLeftOrRightLabel(c, element);
for (RelationshipType relationshipType : relationshipTypeList) {
for (Relationship relationship : relationshipService
.findByItemAndRelationshipType(c, item, relationshipType)) {
@@ -674,7 +669,7 @@ public class MetadataImport {
relationshipService.update(c, relationship);
}
}
handleRelationMetadata(c, item, schema, element, qualifier, language, values, authorities, confidences);
addRelationships(c, item, element, values);
} else {
itemService.clearMetadata(c, item, schema, element, qualifier, language);
itemService.addMetadata(c, item, schema, element, qualifier,
@@ -685,47 +680,35 @@ public class MetadataImport {
}
/**
* This method decides whether the metadatavalue is of type relation.type or if it corresponds to
* a relationship and handles it accordingly to their respective methods
*
* Adds multiple relationships with the same label to an item.
*
* @param c The relevant DSpace context
* @param item The item to which this metadatavalue belongs to
* @param schema The schema for the metadatavalue
* @param element The element for the metadatavalue
* @param qualifier The qualifier for the metadatavalue
* @param language The language for the metadatavalue
* @param values The values for the metadatavalue
* @param authorities The authorities for the metadatavalue
* @param confidences The confidences for the metadatavalue
* @param label The element for the metadatavalue
* @param values to iterate over
* @throws SQLException If something goes wrong
* @throws AuthorizeException If something goes wrong
*/
private void handleRelationMetadata(Context c, Item item, String schema, String element, String qualifier,
String language, List<String> values, List<String> authorities,
List<Integer> confidences) throws SQLException, AuthorizeException,
private void addRelationships(Context c, Item item, String label, List<String> values)
throws SQLException, AuthorizeException,
MetadataImportException {
if (StringUtils.equals(element, "type") && StringUtils.isBlank(qualifier)) {
handleRelationTypeMetadata(c, item, schema, element, qualifier, language, values, authorities, confidences);
} else {
for (String value : values) {
handleRelationOtherMetadata(c, item, element, value);
}
for (String value : values) {
addRelationship(c, item, label, value);
}
}
/**
* Gets an existing entity from a reference.
* Gets an existing entity from a target reference.
*
* @param context the context to use.
* @param reference the reference which may be a UUID, metadata reference, or rowName reference.
* @param targetReference the target reference which may be a UUID, metadata reference, or rowName reference.
* @return the entity, which is guaranteed to exist.
* @throws MetadataImportException if the reference is badly formed or refers to a non-existing item.
* @throws MetadataImportException if the target reference is badly formed or refers to a non-existing item.
*/
private Entity getEntity(Context context, String reference) throws MetadataImportException {
private Entity getEntity(Context context, String targetReference) throws MetadataImportException {
Entity entity = null;
UUID uuid = resolveEntityRef(context, reference);
UUID uuid = resolveEntityRef(context, targetReference);
// At this point, we have a uuid, so we can get an entity
try {
entity = entityService.findByItemId(context, uuid);
@@ -734,218 +717,73 @@ public class MetadataImport {
}
return entity;
} catch (SQLException sqle) {
throw new MetadataImportException("Unable to find entity using reference: " + reference, sqle);
throw new MetadataImportException("Unable to find entity using reference: " + targetReference, sqle);
}
}
/**
* This method takes the item, element and values to determine what relationships should be built
* for these parameters and calls on the method to construct them
*
* Creates a relationship for the given item
*
* @param c The relevant DSpace context
* @param item The item that the relationships will be made for
* @param element The string determining which relationshiptype is to be used
* @param label The relationship label
* @param value The value for the relationship
* @throws SQLException If something goes wrong
* @throws AuthorizeException If something goes wrong
*/
private void handleRelationOtherMetadata(Context c, Item item, String element, String value)
private void addRelationship(Context c, Item item, String label, String value)
throws SQLException, AuthorizeException, MetadataImportException {
if (value.isEmpty()) {
return;
}
Entity entity = entityService.findByItemId(c, item.getID());
boolean left = false;
List<RelationshipType> acceptableRelationshipTypes = new LinkedList<>();
// Get entity from target reference
Entity relationEntity = getEntity(c, value);
// Get relationship type of entity and item
String relationEntityRelationshipType = itemService.getMetadata(relationEntity.getItem(),
"relationship", "type",
null, Item.ANY).get(0).getValue();
String itemRelationshipType = itemService.getMetadata(item, "relationship", "type",
null, Item.ANY).get(0).getValue();
List<RelationshipType> leftRelationshipTypesForEntity = entityService.getLeftRelationshipTypes(c, entity);
List<RelationshipType> rightRelationshipTypesForEntity = entityService.getRightRelationshipTypes(c, entity);
// Get the correct RelationshipType based on label
List<RelationshipType> relType = relationshipTypeService.findByLeftOrRightLabel(c, label);
RelationshipType foundRelationshipType = matchRelationshipType(relType,
itemRelationshipType,relationEntityRelationshipType);
//Identify which RelationshipType objects match the combination of:
// * the left entity type
// * the right entity type
// * the name of the relationship type (based on the expected direction of the relationship)
//The matches are included in the acceptableRelationshipTypes
for (RelationshipType relationshipType : entityService.getAllRelationshipTypes(c, entity)) {
if (StringUtils.equalsIgnoreCase(relationshipType.getLeftwardType(), element)) {
left = verifyValidLeftwardRelationshipType(c, entity, relationEntity, left,
acceptableRelationshipTypes,
leftRelationshipTypesForEntity,
relationshipType);
} else if (StringUtils.equalsIgnoreCase(relationshipType.getRightwardType(), element)) {
left = verifyValidRightwardRelationshipType(c, entity, relationEntity, left,
acceptableRelationshipTypes,
rightRelationshipTypesForEntity,
relationshipType);
}
if (foundRelationshipType == null) {
throw new MetadataImportException("Error on CSV row " + rowCount + ":" + "\n" +
"No Relationship type found for:\n" +
"Target type: " + relationEntityRelationshipType + "\n" +
"Origin referer type: " + itemRelationshipType + "\n" +
"with label: " + label);
}
if (acceptableRelationshipTypes.size() > 1) {
log.error("Ambiguous relationship_types were found");
return;
}
if (acceptableRelationshipTypes.size() == 0) {
log.error("no relationship_types were found");
return;
if (foundRelationshipType.getLeftLabel().equalsIgnoreCase(label)) {
left = true;
}
//There is exactly one
buildRelationObject(c, item, relationEntity.getItem(), left, acceptableRelationshipTypes.get(0));
}
/**
* This method creates the relationship for the item and stores it in the database
* @param c The relevant DSpace context
* @param item The item for which this relationship will be constructed
* @param otherItem The item for the relationship
* @param left A boolean indicating whether the item is the leftItem or the rightItem
* @param acceptedRelationshipType The acceptable relationship type
* @throws SQLException If something goes wrong
* @throws AuthorizeException If something goes wrong
*/
private void buildRelationObject(Context c, Item item, Item otherItem, boolean left,
RelationshipType acceptedRelationshipType)
throws SQLException, AuthorizeException {
// Placeholder items for relation placing
Item leftItem = null;
Item rightItem = null;
if (left) {
leftItem = item;
rightItem = otherItem;
rightItem = relationEntity.getItem();
} else {
leftItem = relationEntity.getItem();
rightItem = item;
leftItem = otherItem;
}
RelationshipType relationshipType = acceptedRelationshipType;
// Create the relationship
int leftPlace = relationshipService.findLeftPlaceByLeftItem(c, leftItem) + 1;
int rightPlace = relationshipService.findRightPlaceByRightItem(c, rightItem) + 1;
Relationship persistedRelationship = relationshipService.create(c, leftItem, rightItem,
relationshipType, leftPlace, rightPlace);
foundRelationshipType, leftPlace, rightPlace);
relationshipService.update(c, persistedRelationship);
}
/**
* This method will add RelationshipType objects to the acceptableRelationshipTypes list
* if applicable and valid RelationshipType objects are found. It will also return a boolean indicating
* whether we're dealing with a left Relationship or not
* @param c The relevant DSpace context
* @param entity The Entity for which the RelationshipType has to be checked
* @param relationEntity The other Entity of the Relationship
* @param left Boolean indicating whether the Relationship is left or not
* @param acceptableRelationshipTypes The list of RelationshipType objects that will be added to
* @param rightRelationshipTypesForEntity The list of RelationshipType objects that are possible
* for the right entity
* @param relationshipType The RelationshipType object that we want to check whether it's
* valid to be added or not
* @return A boolean indicating whether the relationship is left or right.
* Will be set to false if the relationship is valid.
* Will remain unmodified from the left parameter if the
* relationship is not valid.
* @throws SQLException If something goes wrong
*/
private boolean verifyValidRightwardRelationshipType(Context c, Entity entity,
Entity relationEntity,
boolean left,
List<RelationshipType>
acceptableRelationshipTypes,
List<RelationshipType>
rightRelationshipTypesForEntity,
RelationshipType relationshipType)
throws SQLException {
if (StringUtils.equalsIgnoreCase(entityService.getType(c, entity).getLabel(),
relationshipType.getRightType().getLabel()) &&
StringUtils.equalsIgnoreCase(entityService.getType(c, relationEntity).getLabel(),
relationshipType.getLeftType().getLabel())) {
for (RelationshipType rightRelationshipType : rightRelationshipTypesForEntity) {
if (StringUtils.equalsIgnoreCase(rightRelationshipType.getLeftType().getLabel(),
relationshipType.getLeftType().getLabel()) ||
StringUtils.equalsIgnoreCase(rightRelationshipType.getRightType().getLabel(),
relationshipType.getLeftType().getLabel())) {
left = false;
acceptableRelationshipTypes.add(relationshipType);
}
}
}
return left;
}
/**
* This method will add RelationshipType objects to the acceptableRelationshipTypes list
* if applicable and valid RelationshipType objects are found. It will also return a boolean indicating
* whether we're dealing with a left Relationship or not
* @param c The relevant DSpace context
* @param entity The Entity for which the RelationshipType has to be checked
* @param relationEntity The other Entity of the Relationship
* @param left Boolean indicating whether the Relationship is left or not
* @param acceptableRelationshipTypes The list of RelationshipType objects that will be added to
* @param leftRelationshipTypesForEntity The list of RelationshipType objects that are possible
* for the left entity
* @param relationshipType The RelationshipType object that we want to check whether it's
* valid to be added or not
* @return A boolean indicating whether the relationship is left or right.
* Will be set to true if the relationship is valid.
* Will remain unmodified from the left parameter if the
* relationship is not valid.
* @throws SQLException If something goes wrong
*/
private boolean verifyValidLeftwardRelationshipType(Context c, Entity entity,
Entity relationEntity,
boolean left,
List<RelationshipType>
acceptableRelationshipTypes,
List<RelationshipType>
leftRelationshipTypesForEntity,
RelationshipType relationshipType)
throws SQLException {
if (StringUtils.equalsIgnoreCase(entityService.getType(c, entity).getLabel(),
relationshipType.getLeftType().getLabel()) &&
StringUtils.equalsIgnoreCase(entityService.getType(c, relationEntity).getLabel(),
relationshipType.getRightType().getLabel())) {
for (RelationshipType leftRelationshipType : leftRelationshipTypesForEntity) {
if (StringUtils.equalsIgnoreCase(leftRelationshipType.getRightType().getLabel(),
relationshipType.getRightType().getLabel()) ||
StringUtils.equalsIgnoreCase(leftRelationshipType.getLeftType().getLabel(),
relationshipType.getRightType().getLabel())) {
left = true;
acceptableRelationshipTypes.add(relationshipType);
}
}
}
return left;
}
/**
* This method will add the relationship.type metadata to the item if an EntityType can be found for the value in
* the values list.
* @param c The relevant DSpace context
* @param item The item to which this metadatavalue will be added
* @param schema The schema for the metadatavalue to be added
* @param element The element for the metadatavalue to be added
* @param qualifier The qualifier for the metadatavalue to be added
* @param language The language for the metadatavalue to be added
* @param values The value on which we'll search for EntityType object and it's the value
* for the metadatavalue that will be created
* @param authorities The authority for the metadatavalue. This will be filled with the ID
* of the found EntityType for the value if it exists
* @param confidences The confidence for the metadatavalue
* @throws SQLException If something goes wrong
* @throws AuthorizeException If something goes wrong
*/
private void handleRelationTypeMetadata(Context c, Item item, String schema, String element, String qualifier,
String language, List<String> values, List<String> authorities,
List<Integer> confidences)
throws SQLException, AuthorizeException {
EntityType entityType = entityTypeService.findByEntityType(c, values.get(0));
if (entityType != null) {
authorities.add(String.valueOf(entityType.getID()));
itemService.clearMetadata(c, item, schema, element, qualifier, language);
itemService.addMetadata(c, item, schema, element, qualifier, language,
values, authorities, confidences);
itemService.update(c, item);
}
}
/**
* Compare changes between an items owning collection and mapped collections
* and what is in the CSV file
@@ -1586,9 +1424,6 @@ public class MetadataImport {
// Display the changes
displayChanges(changes, true);
// Commit the change to the DB
// c.commit();
}
// Finsh off and tidy up
@@ -1603,24 +1438,28 @@ public class MetadataImport {
}
/**
* Gets a copy of the given csv line with all entity references resolved to UUID strings.
* Gets a copy of the given csv line with all entity target references resolved to UUID strings.
* Keys being iterated over represent metadatafields or special columns to be processed.
*
* @param line the csv line to process.
* @return a copy, with all references resolved.
* @throws MetadataImportException if there is an error resolving any entity reference.
* @throws MetadataImportException if there is an error resolving any entity target reference.
*/
public DSpaceCSVLine resolveEntityRefs(DSpaceCSVLine line) throws MetadataImportException {
DSpaceCSVLine newLine = new DSpaceCSVLine(line.getID());
UUID originId = evaluateOriginId(line.getID());
for (String key : line.keys()) {
// If a key represents a relation field attempt to resolve the reference from the csvRefMap
// If a key represents a relation field attempt to resolve the target reference from the csvRefMap
if (key.split("\\.")[0].equalsIgnoreCase("relation")) {
if (line.get(key).size() > 0) {
for (String val : line.get(key)) {
// Attempt to resolve the relation reference
// These can be a UUID, metadata reference or rowName reference
// Attempt to resolve the relation target reference
// These can be a UUID, metadata target reference or rowName target reference
String uuid = resolveEntityRef(c, val).toString();
newLine.add(key, uuid);
//Entity refs have been resolved / placeholdered
//Populate the EntityRelationMap
populateEntityRelationMap(uuid, key, originId.toString());
}
}
} else {
@@ -1635,27 +1474,59 @@ public class MetadataImport {
}
}
}
return newLine;
}
/**
* Populates the csvRefMap and csvRowMap for the given csv line.
* Populate the entityRelationMap with all target references and it's asscoiated labels
* to their respective origins
*
* @param refUUID the target reference UUID for the relation
* @param relationField the field of the label to relate
*/
private void populateEntityRelationMap(String refUUID, String relationField, String originId) {
HashMap<String, ArrayList<String>> labels = null;
if (entityRelationMap.get(refUUID) == null) {
labels = new HashMap<>();
ArrayList<String> originIds = new ArrayList<>();
originIds.add(originId);
labels.put(relationField, originIds);
entityRelationMap.put(refUUID, labels);
} else {
labels = entityRelationMap.get(refUUID);
if (labels.get(relationField) == null) {
ArrayList<String> originIds = new ArrayList<>();
originIds.add(originId);
labels.put(relationField, originIds);
} else {
ArrayList<String> originIds = labels.get(relationField);
originIds.add(originId);
labels.put(relationField, originIds);
}
entityRelationMap.put(refUUID, labels);
}
}
/**
* Populates the csvRefMap, csvRowMap, and entityTypeMap for the given csv line.
*
* The csvRefMap is an index that keeps track of which rows have a specific value for
* a specific metadata field or the special "rowName" column. This is used to help resolve indirect
* entity references in the same CSV.
* entity target references in the same CSV.
*
* The csvRowMap is a row number to UUID map, and contains an entry for every row that has
* been processed so far which has a known (minted) UUID for its item. This is used to help complete
* the resolution after the row number has been determined.
*
* @param line the csv line.
* @param rowNumber the row number.
* @param uuid the uuid of the item, which may be null if it has not been minted yet.
*/
private void populateRefAndRowMap(DSpaceCSVLine line, int rowNumber, @Nullable UUID uuid) {
private void populateRefAndRowMap(DSpaceCSVLine line, @Nullable UUID uuid) {
if (uuid != null) {
csvRowMap.put(rowNumber, uuid);
csvRowMap.put(rowCount, uuid);
} else {
csvRowMap.put(rowCount, new UUID(0, rowCount));
}
for (String key : line.keys()) {
if (key.contains(".") && !key.split("\\.")[0].equalsIgnoreCase("relation") ||
@@ -1667,22 +1538,31 @@ public class MetadataImport {
rowNums = new HashSet<>();
csvRefMap.put(valueKey, rowNums);
}
rowNums.add(rowNumber);
rowNums.add(rowCount);
}
}
//Populate entityTypeMap
if (key.equalsIgnoreCase("relationship.type") && line.get(key).size() > 0) {
if (uuid == null) {
entityTypeMap.put(new UUID(0, rowCount), line.get(key).get(0));
} else {
entityTypeMap.put(uuid, line.get(key).get(0));
}
}
}
}
/**
* Gets the UUID of the item indicated by the given reference, which may be a direct UUID string, a row reference
* Gets the UUID of the item indicated by the given target reference,
* which may be a direct UUID string, a row reference
* of the form rowName:VALUE, or a metadata value reference of the form schema.element[.qualifier]:VALUE.
*
* The reference may refer to a previously-processed item in the CSV or an item in the database.
*
* @param context the context to use.
* @param reference the reference which may be a UUID, metadata reference, or rowName reference.
* @param reference the target reference which may be a UUID, metadata reference, or rowName reference.
* @return the uuid.
* @throws MetadataImportException if the reference is malformed or ambiguous (refers to multiple items).
* @throws MetadataImportException if the target reference is malformed or ambiguous (refers to multiple items).
*/
private UUID resolveEntityRef(Context context, String reference) throws MetadataImportException {
// value reference
@@ -1692,7 +1572,8 @@ public class MetadataImport {
try {
return UUID.fromString(reference);
} catch (IllegalArgumentException e) {
throw new MetadataImportException("Not a UUID or indirect entity reference: '" + reference + "'");
throw new MetadataImportException("Error in CSV row " + rowCount + ":\n" +
"Not a UUID or indirect entity reference: '" + reference + "'");
}
} else if (!reference.startsWith("rowName:") ) { // Not a rowName ref; so it's a metadata value reference
MetadataValueService metadataValueService = ContentServiceFactory.getInstance().getMetadataValueService();
@@ -1702,7 +1583,8 @@ public class MetadataImport {
String mfValue = reference.substring(i + 1);
String mf[] = reference.substring(0, i).split("\\.");
if (mf.length < 2) {
throw new MetadataImportException("Bad metadata field in reference: '" + reference
throw new MetadataImportException("Error in CSV row " + rowCount + ":\n" +
"Bad metadata field in reference: '" + reference
+ "' (expected syntax is schema.element[.qualifier])");
}
String schema = mf[0];
@@ -1715,11 +1597,13 @@ public class MetadataImport {
MetadataValue mdvVal = mdv.next();
uuid = mdvVal.getDSpaceObject().getID();
if (mdv.hasNext()) {
throw new MetadataImportException("Ambiguous reference; multiple matches in db: " + reference);
throw new MetadataImportException("Error in CSV row " + rowCount + ":\n" +
"Ambiguous reference; multiple matches in db: " + reference);
}
}
} catch (SQLException e) {
throw new MetadataImportException("Error looking up item by metadata reference: " + reference, e);
throw new MetadataImportException("Error in CSV row " + rowCount + ":\n" +
"Error looking up item by metadata reference: " + reference, e);
}
}
// Lookup UUIDs that may have already been processed into the csvRefMap
@@ -1727,20 +1611,23 @@ public class MetadataImport {
// See getMatchingCSVUUIDs() for how the reference param is sourced from the csvRefMap
Set<UUID> csvUUIDs = getMatchingCSVUUIDs(reference);
if (csvUUIDs.size() > 1) {
throw new MetadataImportException("Ambiguous reference; multiple matches in csv: " + reference);
throw new MetadataImportException("Error in CSV row " + rowCount + ":\n" +
"Ambiguous reference; multiple matches in csv: " + reference);
} else if (csvUUIDs.size() == 1) {
UUID csvUUID = csvUUIDs.iterator().next();
if (csvUUID.equals(uuid)) {
return uuid; // one match from csv and db (same item)
} else if (uuid != null) {
throw new MetadataImportException("Ambiguous reference; multiple matches in db and csv: " + reference);
throw new MetadataImportException("Error in CSV row " + rowCount + ":\n" +
"Ambiguous reference; multiple matches in db and csv: " + reference);
} else {
return csvUUID; // one match from csv
}
} else { // size == 0; the reference does not exist throw an error
if (uuid == null) {
throw new MetadataImportException("No matches found for reference: " + reference
+ ", Keep in mind you can only reference entries that are listed before " +
throw new MetadataImportException("Error in CSV row " + rowCount + ":\n" +
"No matches found for reference: " + reference
+ "\nKeep in mind you can only reference entries that are listed before " +
"this one within the CSV.");
} else {
return uuid; // one match from db
@@ -1780,4 +1667,167 @@ public class MetadataImport {
}
}
/**
* Return a UUID of the origin in process or a placeholder for the origin to be evaluated later
*
* @param originId UUID of the origin
* @return the UUID of the item or UUID placeholder
*/
private UUID evaluateOriginId(@Nullable UUID originId) {
if (originId != null) {
return originId;
} else {
return new UUID(0, rowCount);
}
}
/**
* Validate every relation modification expressed in the CSV.
*
*/
private void validateExpressedRelations() throws MetadataImportException {
for (String targetUUID : entityRelationMap.keySet()) {
String targetType = null;
try {
// Get the type of reference. Attempt lookup in processed map first before looking in archive.
if (entityTypeMap.get(UUID.fromString(targetUUID)) != null) {
targetType = entityTypeService.
findByEntityType(c, entityTypeMap.get(UUID.fromString(targetUUID))).getLabel();
} else {
// Target item may be archived; check there.
// Add to errors if Realtionship.type cannot be derived
Item targetItem = null;
if (itemService.find(c, UUID.fromString(targetUUID)) != null) {
targetItem = itemService.find(c, UUID.fromString(targetUUID));
List<MetadataValue> relTypes = itemService.
getMetadata(targetItem, "relationship", "type", null, Item.ANY);
String relTypeValue = null;
if (relTypes.size() > 0) {
relTypeValue = relTypes.get(0).getValue();
targetType = entityTypeService.findByEntityType(c, relTypeValue).getLabel();
} else {
relationValidationErrors.add("Cannot resolve Entity type for target UUID: " +
targetUUID);
}
} else {
relationValidationErrors.add("Cannot resolve Entity type for target UUID: " +
targetUUID);
}
}
if (targetType == null) {
continue;
}
// Get labels for each origin referer of this target.
for (String label : entityRelationMap.get(targetUUID).keySet()) {
// Resolve Entity Type for each origin referer.
for (String originRefererUUID : entityRelationMap.get(targetUUID).get(label)) {
// Evaluate row number for origin referer.
String originRow = "N/A";
if (csvRowMap.containsValue(UUID.fromString(originRefererUUID))) {
for (int key : csvRowMap.keySet()) {
if (csvRowMap.get(key).toString().equalsIgnoreCase(originRefererUUID)) {
originRow = key + "";
break;
}
}
}
String originType = "";
// Validate target type and origin type pairing with label or add to errors.
// Attempt lookup in processed map first before looking in archive.
if (entityTypeMap.get(UUID.fromString(originRefererUUID)) != null) {
originType = entityTypeMap.get(UUID.fromString(originRefererUUID));
validateTypesByTypeByLabel(targetType, originType, label, originRow);
} else {
// Origin item may be archived; check there.
// Add to errors if Realtionship.type cannot be derived.
Item originItem = null;
if (itemService.find(c, UUID.fromString(targetUUID)) != null) {
originItem = itemService.find(c, UUID.fromString(originRefererUUID));
List<MetadataValue> relTypes = itemService.
getMetadata(originItem, "relationship", "type", null, Item.ANY);
String relTypeValue = null;
if (relTypes.size() > 0) {
relTypeValue = relTypes.get(0).getValue();
originType = entityTypeService.findByEntityType(c, relTypeValue).getLabel();
validateTypesByTypeByLabel(targetType, originType, label, originRow);
} else {
relationValidationErrors.add("Error on CSV row " + originRow + ":" + "\n" +
"Cannot resolve Entity type for reference: "
+ originRefererUUID);
}
} else {
relationValidationErrors.add("Error on CSV row " + originRow + ":" + "\n" +
"Cannot resolve Entity type for reference: "
+ originRefererUUID + " in row: " + originRow );
}
}
}
}
} catch (SQLException sqle) {
throw new MetadataImportException("Error interacting with database!", sqle);
}
} // If relationValidationErrors is empty all described relationships are valid.
if (!relationValidationErrors.isEmpty()) {
StringBuilder errors = new StringBuilder();
for (String error : relationValidationErrors) {
errors.append(error + "\n");
}
throw new MetadataImportException("Error validating relationships: \n" + errors);
}
}
/**
* Generates a list of potenital Relationship Types given a label and attempts to match the given
* targetType and originType to a Relationship Type in the list.
*
* @param targetType entity type of target.
* @param originType entity type of origin referer.
* @param label left or right label of the respective Relationship.
* @return the UUID of the item.
*/
private void validateTypesByTypeByLabel(String targetType, String originType, String label, String originRow)
throws MetadataImportException {
try {
RelationshipType foundRelationshipType = null;
List<RelationshipType> relationshipTypeList = relationshipTypeService.
findByLeftOrRightLabel(c, label.split("\\.")[1]);
// Validate described relationship form the CSV.
foundRelationshipType = matchRelationshipType(relationshipTypeList, targetType, originType);
if (foundRelationshipType == null) {
relationValidationErrors.add("Error on CSV row " + originRow + ":" + "\n" +
"No Relationship type found for:\n" +
"Target type: " + targetType + "\n" +
"Origin referer type: " + originType + "\n" +
"with label: " + label);
}
} catch (SQLException sqle) {
throw new MetadataImportException("Error interacting with database!", sqle);
}
}
/**
* Matches two Entity types to a Relationship Type from a set of Relationship Types.
*
* @param relTypes set of Relationship Types.
* @param typeOne first type to be used in matching.
* @param typeTwo second item to be used in matching.
* @return null or matched Relationship Type.
*/
private RelationshipType matchRelationshipType(List<RelationshipType> relTypes,
String typeOne, String typeTwo) {
RelationshipType foundRelationshipType = null;
for (RelationshipType relationshipType : relTypes) {
if (relationshipType.getLeftType().getLabel().equalsIgnoreCase(typeOne) &&
relationshipType.getRightType().getLabel().equalsIgnoreCase(typeTwo) ||
relationshipType.getLeftType().getLabel().equalsIgnoreCase(typeTwo) &&
relationshipType.getRightType().getLabel().equalsIgnoreCase(typeOne)) {
foundRelationshipType = relationshipType;
}
}
return foundRelationshipType;
}
}

View File

@@ -33,7 +33,6 @@ import org.dspace.content.service.ItemService;
import org.dspace.content.service.MetadataFieldService;
import org.dspace.content.service.MetadataValueService;
import org.dspace.content.service.RelationshipService;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -63,13 +62,6 @@ public class CSVMetadataImportReferenceTest extends AbstractEntityIntegrationTes
.withName("Parent Community")
.build();
col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build();
}
/**
* Close testing enviorment and restore auth system
*/
@After
public void close() {
context.restoreAuthSystemState();
}
@@ -180,9 +172,11 @@ public class CSVMetadataImportReferenceTest extends AbstractEntityIntegrationTes
*/
@Test
public void testSingleUUIDReference() throws Exception {
context.turnOffAuthorisationSystem();
Item person = ItemBuilder.createItem(context, col1)
.withRelationshipType("Person")
.build();
context.restoreAuthSystemState();
String[] csv = {"id,relationship.type,relation.isAuthorOfPublication,collection,rowName,dc.identifier.other",
"+,Publication," + person.getID().toString() + "," + col1.getHandle() + ",anything,0"};
Item[] items = runImport(csv);
@@ -195,12 +189,14 @@ public class CSVMetadataImportReferenceTest extends AbstractEntityIntegrationTes
*/
@Test
public void testMultiUUIDReference() throws Exception {
context.turnOffAuthorisationSystem();
Item person = ItemBuilder.createItem(context, col1)
.withRelationshipType("Person")
.build();
Item person2 = ItemBuilder.createItem(context, col1)
.withRelationshipType("Person")
.build();
context.restoreAuthSystemState();
String[] csv = {"id,relationship.type,relation.isAuthorOfPublication,collection,rowName,dc.identifier.other",
"+,Publication," + person.getID().toString() + "||" + person2.getID().toString() + "," +
col1.getHandle() + ",anything,0"};
@@ -215,6 +211,7 @@ public class CSVMetadataImportReferenceTest extends AbstractEntityIntegrationTes
*/
@Test
public void testMultiRefArchivedCsv() throws Exception {
context.turnOffAuthorisationSystem();
Item person = ItemBuilder.createItem(context, col1)
.withTitle("Person")
.withRelationshipType("Person")
@@ -223,6 +220,7 @@ public class CSVMetadataImportReferenceTest extends AbstractEntityIntegrationTes
"dc.identifier.other",
"+,Person2,Person,," + col1.getHandle() + ",idVal,0",
"+,Pub1,Publication,dc.title:Person||dc.title:Person2," + col1.getHandle() + ",anything,1"};
context.restoreAuthSystemState();
Item[] items = runImport(csv);
assertRelationship(items[1], person, 1, "left", 0);
assertRelationship(items[1], items[0], 1, "left", 1);
@@ -235,6 +233,7 @@ public class CSVMetadataImportReferenceTest extends AbstractEntityIntegrationTes
*/
@Test
public void testMultiMixedRefArchivedCsv() throws Exception {
context.turnOffAuthorisationSystem();
Item person = ItemBuilder.createItem(context, col1)
.withTitle("Person")
.withRelationshipType("Person")
@@ -243,6 +242,7 @@ public class CSVMetadataImportReferenceTest extends AbstractEntityIntegrationTes
.withTitle("Person2")
.withRelationshipType("Person")
.build();
context.restoreAuthSystemState();
String[] csv = {"id,dc.title,relationship.type,relation.isAuthorOfPublication,collection,rowName," +
"dc.identifier.other",
"+,Person3,Person,," + col1.getHandle() + ",idVal,0",
@@ -297,6 +297,7 @@ public class CSVMetadataImportReferenceTest extends AbstractEntityIntegrationTes
*/
@Test
public void testNonUniqueMDRefInDb() throws Exception {
context.turnOffAuthorisationSystem();
ItemBuilder.createItem(context, col1)
.withRelationshipType("Person")
.withIdentifierOther("1")
@@ -305,6 +306,7 @@ public class CSVMetadataImportReferenceTest extends AbstractEntityIntegrationTes
.withRelationshipType("Person")
.withIdentifierOther("1")
.build();
context.restoreAuthSystemState();
String[] csv = {"id,relationship.type,relation.isAuthorOfPublication,collection,dc.identifier.other",
"+,Publication,dc.identifier.other:1," + col1.getHandle() + ",2"};
assertEquals(1, performImportScript(csv, true));
@@ -315,10 +317,12 @@ public class CSVMetadataImportReferenceTest extends AbstractEntityIntegrationTes
*/
@Test
public void testNonUniqueMDRefInBoth() throws Exception {
context.turnOffAuthorisationSystem();
ItemBuilder.createItem(context, col1)
.withRelationshipType("Person")
.withIdentifierOther("1")
.build();
context.restoreAuthSystemState();
String[] csv = {"id,relationship.type,relation.isAuthorOfPublication,collection,dc.identifier.other",
"+,Person,," + col1.getHandle() + ",1",
"+,Publication,dc.identifier.other:1," + col1.getHandle() + ",2"};
@@ -358,6 +362,78 @@ public class CSVMetadataImportReferenceTest extends AbstractEntityIntegrationTes
assertEquals(1, performImportScript(csv, false));
}
/**
* Test relationship validation with invalid relationship definition
*/
@Test
public void testCSVImportInvalidRelationship() throws Exception {
String[] csv = {"id,relationship.type,relation.isAuthorOfPublication,collection,rowName",
"+,Publication,," + col1.getHandle() + ",row1",
"+,Unit,rowName:row1," + col1.getHandle() + ",row2",};
assertEquals(1, performImportScript(csv, true));
}
/**
* Test relationship validation with invalid relationship definition and with an archived origin referer
*/
@Test
public void testInvalidRelationshipArchivedOrigin() throws Exception {
context.turnOffAuthorisationSystem();
Item testItem = ItemBuilder.createItem(context, col1)
.withRelationshipType("OrgUnit")
.build();
context.restoreAuthSystemState();
String[] csv = {"id,relationship.type,relation.isAuthorOfPublication,collection,rowName",
"+,Person,," + col1.getHandle() + ",1",
testItem.getID().toString() + ",,rowName:1," + col1.getHandle() + ",2"};
assertEquals(1, performImportScript(csv, false));
}
/**
* Test relationship validation with invalid relationship definition and with archived target reference
*/
@Test
public void testInvalidRelationshipArchivedTarget() throws Exception {
context.turnOffAuthorisationSystem();
Item testItem = ItemBuilder.createItem(context, col1)
.withRelationshipType("OrgUnit")
.build();
context.restoreAuthSystemState();
String[] csv = {"id,relationship.type,relation.isAuthorOfPublication,collection,rowName",
testItem.getID().toString() + ",Person,," + col1.getHandle() + ",1",
"+,OrgUnit,rowName:1," + col1.getHandle() + ",2"};
assertEquals(1, performImportScript(csv, false));
}
/**
* Test relationship validation against archived items with pre-existing relationship types
*/
@Test
public void testValidRelationshipNoDefinedTypesInCSV() throws Exception {
context.turnOffAuthorisationSystem();
Item testItemOne = ItemBuilder.createItem(context, col1)
.withRelationshipType("Person")
.withIdentifierOther("testItemOne")
.build();
Item testItemTwo = ItemBuilder.createItem(context, col1)
.withRelationshipType("Publication")
.withIdentifierOther("testItemTwo")
.build();
Item testItemThree = ItemBuilder.createItem(context, col1)
.withRelationshipType("Project")
.withIdentifierOther("testItemThree")
.build();
context.restoreAuthSystemState();
String[] csv = {"id,relation.isAuthorOfPublication,relation.isPublicationOfProject,collection",
testItemOne.getID().toString() + ",,," + col1.getHandle(),
testItemTwo.getID().toString() + ",dc.identifier.other:testItemOne,," + col1.getHandle(),
testItemThree.getID().toString() + ",,dc.identifier.other:testItemTwo," + col1.getHandle()};
performImportScript(csv, false);
assertRelationship(testItemTwo, testItemOne, 1, "left", 0);
assertRelationship(testItemTwo, testItemThree, 1, "left", 0);
}
/**
* Import mocked CSVs to test item creation behavior, deleting temporary file afterward.

View File

@@ -26,7 +26,7 @@ dspace.dir = /dspace
dspace.hostname = localhost
# DSpace base host URL. Include port number etc.
dspace.baseUrl = http://localhost:8080/server
dspace.baseUrl = http://localhost:8080
# Full link your end users will use to access DSpace. In most cases, this will be the baseurl followed by
# the context path to the UI you are using.
@@ -37,7 +37,7 @@ dspace.url = ${dspace.baseUrl}
# This is the URL that will be used for the REST endpoints to be served on.
# This will typically be followed by /api to determine the root endpoints.
dspace.restUrl = ${dspace.baseUrl}
dspace.restUrl = ${dspace.baseUrl}/server
# Optional: DSpace URL for mobile access
# This