
org.dspace.content.RelationshipServiceImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of dspace-api Show documentation
Show all versions of dspace-api Show documentation
DSpace core data model and service APIs.
The newest version!
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.content;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.content.Relationship.LatestVersionStatus;
import org.dspace.content.dao.RelationshipDAO;
import org.dspace.content.dao.pojo.ItemUuidAndRelationshipId;
import org.dspace.content.service.EntityTypeService;
import org.dspace.content.service.ItemService;
import org.dspace.content.service.RelationshipService;
import org.dspace.content.service.RelationshipTypeService;
import org.dspace.content.virtual.VirtualMetadataConfiguration;
import org.dspace.content.virtual.VirtualMetadataPopulator;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.services.ConfigurationService;
import org.dspace.versioning.utils.RelationshipVersioningUtils;
import org.springframework.beans.factory.annotation.Autowired;
public class RelationshipServiceImpl implements RelationshipService {
private static final Logger log = LogManager.getLogger();
@Autowired(required = true)
protected RelationshipDAO relationshipDAO;
@Autowired(required = true)
protected AuthorizeService authorizeService;
@Autowired(required = true)
protected ItemService itemService;
@Autowired(required = true)
protected RelationshipTypeService relationshipTypeService;
@Autowired
private ConfigurationService configurationService;
@Autowired
private EntityTypeService entityTypeService;
@Autowired
private RelationshipMetadataService relationshipMetadataService;
@Autowired
private RelationshipVersioningUtils relationshipVersioningUtils;
@Autowired
private VirtualMetadataPopulator virtualMetadataPopulator;
@Override
public Relationship create(Context context) throws SQLException, AuthorizeException {
if (!authorizeService.isAdmin(context)) {
throw new AuthorizeException(
"Only administrators can modify relationship");
}
return relationshipDAO.create(context, new Relationship());
}
@Override
public Relationship create(Context c, Item leftItem, Item rightItem, RelationshipType relationshipType,
int leftPlace, int rightPlace) throws AuthorizeException, SQLException {
return create(c, leftItem, rightItem, relationshipType, leftPlace, rightPlace, null, null);
}
@Override
public Relationship create(
Context c, Item leftItem, Item rightItem, RelationshipType relationshipType, int leftPlace, int rightPlace,
String leftwardValue, String rightwardValue, LatestVersionStatus latestVersionStatus
) throws AuthorizeException, SQLException {
Relationship relationship = new Relationship();
relationship.setLeftItem(leftItem);
relationship.setRightItem(rightItem);
relationship.setRelationshipType(relationshipType);
relationship.setLeftPlace(leftPlace);
relationship.setRightPlace(rightPlace);
relationship.setLeftwardValue(leftwardValue);
relationship.setRightwardValue(rightwardValue);
relationship.setLatestVersionStatus(latestVersionStatus);
return create(c, relationship);
}
@Override
public Relationship create(
Context c, Item leftItem, Item rightItem, RelationshipType relationshipType, int leftPlace, int rightPlace,
String leftwardValue, String rightwardValue
) throws AuthorizeException, SQLException {
return create(
c, leftItem, rightItem, relationshipType, leftPlace, rightPlace, leftwardValue, rightwardValue,
LatestVersionStatus.BOTH
);
}
@Override
public Relationship create(Context context, Relationship relationship) throws SQLException, AuthorizeException {
if (isRelationshipValidToCreate(context, relationship)) {
if (authorizeService.authorizeActionBoolean(context, relationship.getLeftItem(), Constants.WRITE) ||
authorizeService.authorizeActionBoolean(context, relationship.getRightItem(), Constants.WRITE)) {
// This order of execution should be handled in the creation (create, updateplace, update relationship)
// for a proper place allocation
Relationship relationshipToReturn = relationshipDAO.create(context, relationship);
updatePlaceInRelationship(context, relationshipToReturn, null, null, true, true);
update(context, relationshipToReturn);
updateItemsInRelationship(context, relationship);
return relationshipToReturn;
} else {
throw new AuthorizeException(
"You do not have write rights on this relationship's items");
}
} else {
throw new IllegalArgumentException("The relationship given was not valid");
}
}
@Override
public Relationship move(
Context context, Relationship relationship, Integer newLeftPlace, Integer newRightPlace
) throws SQLException, AuthorizeException {
if (authorizeService.authorizeActionBoolean(context, relationship.getLeftItem(), Constants.WRITE) ||
authorizeService.authorizeActionBoolean(context, relationship.getRightItem(), Constants.WRITE)) {
// Don't do anything if neither the leftPlace nor rightPlace was updated
if (newLeftPlace != null || newRightPlace != null) {
// This order of execution should be handled in the creation (create, updateplace, update relationship)
// for a proper place allocation
updatePlaceInRelationship(context, relationship, newLeftPlace, newRightPlace, false, false);
update(context, relationship);
updateItemsInRelationship(context, relationship);
}
return relationship;
} else {
throw new AuthorizeException(
"You do not have write rights on this relationship's items");
}
}
@Override
public Relationship move(
Context context, Relationship relationship, Item newLeftItem, Item newRightItem
) throws SQLException, AuthorizeException {
// If the new Item is the same as the current Item, don't move
newLeftItem = newLeftItem != relationship.getLeftItem() ? newLeftItem : null;
newRightItem = newRightItem != relationship.getRightItem() ? newRightItem : null;
// Don't do anything if neither the leftItem nor rightItem was updated
if (newLeftItem != null || newRightItem != null) {
// First move the Relationship to the back within the current Item's lists
// This ensures that we won't have any gaps once we move the Relationship to a different Item
move(
context, relationship,
newLeftItem != null ? -1 : null,
newRightItem != null ? -1 : null
);
boolean insertLeft = false;
boolean insertRight = false;
// If Item has been changed, mark the previous Item as modified to make sure we discard the old relation.*
// metadata on the next update.
// Set the Relationship's Items to the new ones, appending to the end
if (newLeftItem != null) {
relationship.getLeftItem().setMetadataModified();
relationship.setLeftItem(newLeftItem);
relationship.setLeftPlace(-1);
insertLeft = true;
}
if (newRightItem != null) {
relationship.getRightItem().setMetadataModified();
relationship.setRightItem(newRightItem);
relationship.setRightPlace(-1);
insertRight = true;
}
// This order of execution should be handled in the creation (create, updateplace, update relationship)
// for a proper place allocation
updatePlaceInRelationship(context, relationship, null, null, insertLeft, insertRight);
update(context, relationship);
updateItemsInRelationship(context, relationship);
}
return relationship;
}
/**
* This method will update the place for the Relationship and all other relationships found by the items and
* relationship type of the given Relationship.
*
* @param context The relevant DSpace context
* @param relationship The Relationship object that will have its place updated and that will be used
* to retrieve the other relationships whose place might need to be updated.
* @param newLeftPlace If the Relationship in question is to be moved, the leftPlace it is to be moved to.
* Set this to null if the Relationship has not been moved, i.e. it has just been created,
* deleted or when its Items have been modified.
* @param newRightPlace If the Relationship in question is to be moved, the rightPlace it is to be moved to.
* Set this to null if the Relationship has not been moved, i.e. it has just been created,
* deleted or when its Items have been modified.
* @param insertLeft Whether the Relationship in question should be inserted into the left Item.
* Should be set to true when creating or moving to a different Item.
* @param insertRight Whether the Relationship in question should be inserted into the right Item.
* Should be set to true when creating or moving to a different Item.
* @throws SQLException If something goes wrong
* @throws AuthorizeException
* If the user is not authorized to update the Relationship or its Items
*/
private void updatePlaceInRelationship(
Context context, Relationship relationship,
Integer newLeftPlace, Integer newRightPlace, boolean insertLeft, boolean insertRight
) throws SQLException, AuthorizeException {
Item leftItem = relationship.getLeftItem();
Item rightItem = relationship.getRightItem();
// These list also include the non-latest. This is relevant to determine whether it's deleted.
// This can also imply there may be overlapping places, and/or the given relationship will overlap
// But the shift will allow this, and only happen when needed based on the latest status
List leftRelationships = findByItemAndRelationshipType(
context, leftItem, relationship.getRelationshipType(), true, -1, -1, false
);
List rightRelationships = findByItemAndRelationshipType(
context, rightItem, relationship.getRelationshipType(), false, -1, -1, false
);
// These relationships are only deleted from the temporary lists in case they're present in them so that we can
// properly perform our place calculation later down the line in this method.
boolean deletedFromLeft = !leftRelationships.contains(relationship);
boolean deletedFromRight = !rightRelationships.contains(relationship);
leftRelationships.remove(relationship);
rightRelationships.remove(relationship);
List leftMetadata = getSiblingMetadata(leftItem, relationship, true);
List rightMetadata = getSiblingMetadata(rightItem, relationship, false);
// For new relationships added to the end, this will be -1.
// For new relationships added at a specific position, this will contain that position.
// For existing relationships, this will contain the place before it was moved.
// For deleted relationships, this will contain the place before it was deleted.
int oldLeftPlace = relationship.getLeftPlace();
int oldRightPlace = relationship.getRightPlace();
boolean movedUpLeft = resolveRelationshipPlace(
relationship, true, leftRelationships, leftMetadata, oldLeftPlace, newLeftPlace
);
boolean movedUpRight = resolveRelationshipPlace(
relationship, false, rightRelationships, rightMetadata, oldRightPlace, newRightPlace
);
context.turnOffAuthorisationSystem();
//only shift if the place is relevant for the latest relationships
if (relationshipVersioningUtils.otherSideIsLatest(true, relationship.getLatestVersionStatus())) {
shiftSiblings(
relationship, true, oldLeftPlace, movedUpLeft, insertLeft, deletedFromLeft,
leftRelationships, leftMetadata
);
}
if (relationshipVersioningUtils.otherSideIsLatest(false, relationship.getLatestVersionStatus())) {
shiftSiblings(
relationship, false, oldRightPlace, movedUpRight, insertRight, deletedFromRight,
rightRelationships, rightMetadata
);
}
updateItem(context, leftItem);
updateItem(context, rightItem);
context.restoreAuthSystemState();
}
/**
* Return the MDVs in the Item's MDF corresponding to the given Relationship.
* Return an empty list if the Relationship isn't mapped to any MDF
* or if the mapping is configured with useForPlace=false.
*
* This returns actual metadata (not virtual) which in the same metadata field as the useForPlace.
* For a publication with 2 author relationships and 3 plain text dc.contributor.author values,
* it would return the 3 plain text dc.contributor.author values.
* For a person related to publications, it would return an empty list.
*/
private List getSiblingMetadata(
Item item, Relationship relationship, boolean isLeft
) {
List metadata = new ArrayList<>();
if (virtualMetadataPopulator.isUseForPlaceTrueForRelationshipType(relationship.getRelationshipType(), isLeft)) {
HashMap mapping;
if (isLeft) {
mapping = virtualMetadataPopulator.getMap().get(relationship.getRelationshipType().getLeftwardType());
} else {
mapping = virtualMetadataPopulator.getMap().get(relationship.getRelationshipType().getRightwardType());
}
if (mapping != null) {
for (String mdf : mapping.keySet()) {
metadata.addAll(
// Make sure we're only looking at database MDVs; if the relationship currently overlaps
// one of these, its virtual MDV will overwrite the database MDV in itemService.getMetadata()
// The relationship pass should be sufficient to move any sibling virtual MDVs.
item.getMetadata()
.stream()
.filter(mdv -> mdv.getMetadataField().toString().equals(mdf.replace(".", "_")))
.collect(Collectors.toList())
);
}
}
}
return metadata;
}
/**
* Set the left/right place of a Relationship
* - To a new place in case it's being moved
* - Resolve -1 to the actual last place based on the places of its sibling Relationships and/or MDVs
* and determine if it has been moved up in the list.
*
* Examples:
* - Insert a Relationship at place 3
* newPlace starts out as null and is not updated. Return movedUp=false
* - Insert a Relationship at place -1
* newPlace starts out as null and is resolved to e.g. 6. Update the Relationship and return movedUp=false
* - Move a Relationship from place 4 to 2
* Update the Relationship and return movedUp=false.
* - Move a Relationship from place 2 to -1
* newPlace starts out as -1 and is resolved to e.g. 5. Update the relationship and return movedUp=true.
* - Remove a relationship from place 1
* Return movedUp=false
*
* @param relationship the Relationship that's being updated
* @param isLeft whether to consider the left side of the Relationship.
* This method should be called twice, once with isLeft=true and once with isLeft=false.
* Make sure this matches the provided relationships/metadata/oldPlace/newPlace.
* @param relationships the list of sibling Relationships
* @param metadata the list of sibling MDVs
* @param oldPlace the previous place for this Relationship, in case it has been moved.
* Otherwise, the current place of a deleted Relationship
* or the place a Relationship has been inserted.
* @param newPlace The new place for this Relationship. Will be null on insert/delete.
* @return true if the Relationship was moved and newPlace > oldPlace
*/
private boolean resolveRelationshipPlace(
Relationship relationship, boolean isLeft,
List relationships, List metadata,
int oldPlace, Integer newPlace
) {
boolean movedUp = false;
if (newPlace != null) {
// We're moving an existing Relationship...
if (newPlace == -1) {
// ...to the end of the list
int nextPlace = getNextPlace(relationships, metadata, isLeft);
if (nextPlace == oldPlace) {
// If this Relationship is already at the end, do nothing.
newPlace = oldPlace;
} else {
// Subtract 1 from the next place since we're moving, not inserting and
// the total number of Relationships stays the same.
newPlace = nextPlace - 1;
}
}
if (newPlace > oldPlace) {
// ...up the list. We have to keep track of this in order to shift correctly later on
movedUp = true;
}
} else if (oldPlace == -1) {
// We're _not_ moving an existing Relationship. The newPlace is already set in the Relationship object.
// We only need to resolve it to the end of the list if it's set to -1, otherwise we can just keep it as is.
newPlace = getNextPlace(relationships, metadata, isLeft);
}
if (newPlace != null) {
setPlace(relationship, isLeft, newPlace);
}
return movedUp;
}
/**
* Return the index of the next place in a list of Relationships and Metadata.
* By not relying on the size of both lists we can support one-to-many virtual MDV mappings.
* @param isLeft whether to take the left or right place of each Relationship
*/
private int getNextPlace(List relationships, List metadata, boolean isLeft) {
return Stream.concat(
metadata.stream().map(MetadataValue::getPlace),
relationships.stream().map(r -> getPlace(r, isLeft))
).max(Integer::compare)
.map(integer -> integer + 1)
.orElse(0);
}
/**
* Adjust the left/right place of sibling Relationships and MDVs
*
* Examples: with sibling Relationships R,S,T and metadata a,b,c
* - Insert T at place 1 aRbSc -> a T RbSc
* Shift all siblings with place >= 1 one place to the right
* - Delete R from place 2 aT R bSc -> aTbSc
* Shift all siblings with place > 2 one place to the left
* - Move S from place 3 to place 2 (movedUp=false) aTb S c -> aT S bc
* Shift all siblings with 2 < place <= 3 one place to the right
* - Move T from place 1 to place 3 (movedUp=true) a T Sbc -> aSb T c
* Shift all siblings with 1 < place <= 3 one place to the left
*
* @param relationship the Relationship that's being updated
* @param isLeft whether to consider the left side of the Relationship.
* This method should be called twice, once with isLeft=true and once with isLeft=false.
* Make sure this matches the provided relationships/metadata/oldPlace/newPlace.
* @param oldPlace the previous place for this Relationship, in case it has been moved.
* Otherwise, the current place of a deleted Relationship
* or the place a Relationship has been inserted.
* @param movedUp if this Relationship has been moved up the list, e.g. from place 2 to place 4
* @param deleted whether this Relationship has been deleted
* @param relationships the list of sibling Relationships
* @param metadata the list of sibling MDVs
*/
private void shiftSiblings(
Relationship relationship, boolean isLeft, int oldPlace, boolean movedUp, boolean inserted, boolean deleted,
List relationships, List metadata
) {
int newPlace = getPlace(relationship, isLeft);
for (Relationship sibling : relationships) {
// NOTE: If and only if the other side of the relationship has "latest" status, the relationship will appear
// as a metadata value on the item at the current side (indicated by isLeft) of the relationship.
//
// Example: volume <----> issue (LEFT_ONLY)
// => LEFT_ONLY means that the volume has "latest" status, but the issue does NOT have "latest" status
// => the volume will appear in the metadata of the issue,
// but the issue will NOT appear in the metadata of the volume
//
// This means that the other side of the relationship has to have "latest" status, otherwise this
// relationship is NOT relevant for place calculation.
if (relationshipVersioningUtils.otherSideIsLatest(isLeft, sibling.getLatestVersionStatus())) {
int siblingPlace = getPlace(sibling, isLeft);
if (
(deleted && siblingPlace > newPlace)
// If the relationship was deleted, all relationships after it should shift left
// We must make the distinction between deletes and moves because for inserts oldPlace == newPlace
|| (movedUp && siblingPlace <= newPlace && siblingPlace > oldPlace)
// If the relationship was moved up e.g. from place 2 to 5, all relationships
// with place > 2 (the old place) and <= to 5 should shift left
) {
setPlace(sibling, isLeft, siblingPlace - 1);
} else if (
(inserted && siblingPlace >= newPlace)
// If the relationship was inserted, all relationships starting from that place should shift right
// We must make the distinction between inserts and moves because for inserts oldPlace == newPlace
|| (!movedUp && siblingPlace >= newPlace && siblingPlace < oldPlace)
// If the relationship was moved down e.g. from place 5 to 2, all relationships
// with place >= 2 and < 5 (the old place) should shift right
) {
setPlace(sibling, isLeft, siblingPlace + 1);
}
}
}
for (MetadataValue mdv : metadata) {
// NOTE: Plain text metadata values should ALWAYS be included in the place calculation,
// because they are by definition only visible/relevant to the side of the relationship
// that we are currently processing.
int mdvPlace = mdv.getPlace();
if (
(deleted && mdvPlace > newPlace)
// If the relationship was deleted, all metadata after it should shift left
// We must make the distinction between deletes and moves because for inserts oldPlace == newPlace
// If the reltionship was copied to metadata on deletion:
// - the plain text can be after the relationship (in which case it's moved forward again)
// - or before the relationship (in which case it remains in place)
|| (movedUp && mdvPlace <= newPlace && mdvPlace > oldPlace)
// If the relationship was moved up e.g. from place 2 to 5, all metadata
// with place > 2 (the old place) and <= to 5 should shift left
) {
mdv.setPlace(mdvPlace - 1);
} else if (
(inserted && mdvPlace >= newPlace)
// If the relationship was inserted, all relationships starting from that place should shift right
// We must make the distinction between inserts and moves because for inserts oldPlace == newPlace
|| (!movedUp && mdvPlace >= newPlace && mdvPlace < oldPlace)
// If the relationship was moved down e.g. from place 5 to 2, all relationships
// with place >= 2 and < 5 (the old place) should shift right
) {
mdv.setPlace(mdvPlace + 1);
}
}
}
private int getPlace(Relationship relationship, boolean isLeft) {
if (isLeft) {
return relationship.getLeftPlace();
} else {
return relationship.getRightPlace();
}
}
private void setPlace(Relationship relationship, boolean isLeft, int place) {
if (isLeft) {
relationship.setLeftPlace(place);
} else {
relationship.setRightPlace(place);
}
}
@Override
public void updateItem(Context context, Item relatedItem)
throws SQLException, AuthorizeException {
relatedItem.setMetadataModified();
itemService.update(context, relatedItem);
}
private boolean isRelationshipValidToCreate(Context context, Relationship relationship) throws SQLException {
RelationshipType relationshipType = relationship.getRelationshipType();
if (!verifyEntityTypes(relationship.getLeftItem(), relationshipType.getLeftType())) {
log.warn("The relationship has been deemed invalid since the leftItem" +
" and leftType do no match on entityType");
logRelationshipTypeDetailsForError(relationshipType);
return false;
}
if (!verifyEntityTypes(relationship.getRightItem(), relationshipType.getRightType())) {
log.warn("The relationship has been deemed invalid since the rightItem" +
" and rightType do no match on entityType");
logRelationshipTypeDetailsForError(relationshipType);
return false;
}
if (!relationship.getLatestVersionStatus().equals(LatestVersionStatus.LEFT_ONLY)
&& !verifyMaxCardinality(context, relationship.getLeftItem(),
relationshipType.getLeftMaxCardinality(), relationshipType, true)) {
//If RIGHT_ONLY => it's a copied relationship, and the count can be ignored
log.warn("The relationship has been deemed invalid since the left item has more" +
" relationships than the left max cardinality allows after we'd store this relationship");
logRelationshipTypeDetailsForError(relationshipType);
return false;
}
if (!relationship.getLatestVersionStatus().equals(LatestVersionStatus.RIGHT_ONLY)
&& !verifyMaxCardinality(context, relationship.getRightItem(),
relationshipType.getRightMaxCardinality(), relationshipType, false)) {
//If LEFT_ONLY => it's a copied relationship, and the count can be ignored
log.warn("The relationship has been deemed invalid since the right item has more" +
" relationships than the right max cardinality allows after we'd store this relationship");
logRelationshipTypeDetailsForError(relationshipType);
return false;
}
return true;
}
private void logRelationshipTypeDetailsForError(RelationshipType relationshipType) {
log.warn("The relationshipType's ID is: " + relationshipType.getID());
log.warn("The relationshipType's leftward type is: " + relationshipType.getLeftwardType());
log.warn("The relationshipType's rightward type is: " + relationshipType.getRightwardType());
log.warn("The relationshipType's left entityType label is: " + relationshipType.getLeftType().getLabel());
log.warn("The relationshipType's right entityType label is: " + relationshipType.getRightType().getLabel());
log.warn("The relationshipType's left min cardinality is: " + relationshipType.getLeftMinCardinality());
log.warn("The relationshipType's left max cardinality is: " + relationshipType.getLeftMaxCardinality());
log.warn("The relationshipType's right min cardinality is: " + relationshipType.getRightMinCardinality());
log.warn("The relationshipType's right max cardinality is: " + relationshipType.getRightMaxCardinality());
}
private boolean verifyMaxCardinality(Context context, Item itemToProcess,
Integer maxCardinality,
RelationshipType relationshipType,
boolean isLeft) throws SQLException {
if (maxCardinality == null) {
//no need to check the relationships
return true;
}
List rightRelationships = findByItemAndRelationshipType(context, itemToProcess, relationshipType,
isLeft);
if (rightRelationships.size() >= maxCardinality) {
return false;
}
return true;
}
private boolean verifyEntityTypes(Item itemToProcess, EntityType entityTypeToProcess) {
List list = itemService.getMetadata(itemToProcess, "dspace", "entity",
"type", Item.ANY, false);
if (list.isEmpty()) {
return false;
}
String leftEntityType = list.get(0).getValue();
return StringUtils.equals(leftEntityType, entityTypeToProcess.getLabel());
}
@Override
public Relationship find(Context context, int id) throws SQLException {
Relationship relationship = relationshipDAO.findByID(context, Relationship.class, id);
return relationship;
}
@Override
public List findByItem(Context context, Item item) throws SQLException {
return findByItem(context, item, -1, -1, false);
}
@Override
public List findByItem(
Context context, Item item, Integer limit, Integer offset, boolean excludeTilted
) throws SQLException {
return findByItem(context, item, limit, offset, excludeTilted, true);
}
@Override
public List findByItem(
Context context, Item item, Integer limit, Integer offset, boolean excludeTilted, boolean excludeNonLatest
) throws SQLException {
List list =
relationshipDAO.findByItem(context, item, limit, offset, excludeTilted, excludeNonLatest);
list.sort((o1, o2) -> {
int relationshipType = o1.getRelationshipType().getLeftwardType()
.compareTo(o2.getRelationshipType().getLeftwardType());
if (relationshipType != 0) {
return relationshipType;
} else {
if (o1.getLeftItem() == item) {
return o1.getLeftPlace() - o2.getLeftPlace();
} else {
return o1.getRightPlace() - o2.getRightPlace();
}
}
});
return list;
}
@Override
public List findAll(Context context) throws SQLException {
return findAll(context, -1, -1);
}
@Override
public List findAll(Context context, Integer limit, Integer offset) throws SQLException {
return relationshipDAO.findAll(context, Relationship.class, limit, offset);
}
@Override
public void update(Context context, Relationship relationship) throws SQLException, AuthorizeException {
update(context, Collections.singletonList(relationship));
}
@Override
public void update(Context context, List relationships) throws SQLException, AuthorizeException {
if (CollectionUtils.isNotEmpty(relationships)) {
for (Relationship relationship : relationships) {
if (authorizeService.authorizeActionBoolean(context, relationship.getLeftItem(), Constants.WRITE) ||
authorizeService.authorizeActionBoolean(context, relationship.getRightItem(), Constants.WRITE)) {
if (isRelationshipValidToCreate(context, relationship)) {
relationshipDAO.save(context, relationship);
}
} else {
throw new AuthorizeException("You do not have write rights on this relationship's items");
}
}
}
}
@Override
public void delete(Context context, Relationship relationship) throws SQLException, AuthorizeException {
delete(context, relationship, relationship.getRelationshipType().isCopyToLeft(),
relationship.getRelationshipType().isCopyToRight());
}
@Override
public void delete(Context context, Relationship relationship, boolean copyToLeftItem, boolean copyToRightItem)
throws SQLException, AuthorizeException {
log.info(org.dspace.core.LogHelper.getHeader(context, "delete_relationship",
"relationship_id=" + relationship.getID() + "&" +
"copyMetadataValuesToLeftItem=" + copyToLeftItem + "&" +
"copyMetadataValuesToRightItem=" + copyToRightItem));
if (isRelationshipValidToDelete(context, relationship) &&
copyToItemPermissionCheck(context, relationship, copyToLeftItem, copyToRightItem)) {
// To delete a relationship, a user must have WRITE permissions on one of the related Items
deleteRelationshipAndCopyToItem(context, relationship, copyToLeftItem, copyToRightItem);
} else {
throw new IllegalArgumentException("The relationship given was not valid");
}
}
@Override
public void forceDelete(Context context, Relationship relationship, boolean copyToLeftItem, boolean copyToRightItem)
throws SQLException, AuthorizeException {
log.info(org.dspace.core.LogHelper.getHeader(context, "delete_relationship",
"relationship_id=" + relationship.getID() + "&" +
"copyMetadataValuesToLeftItem=" + copyToLeftItem + "&" +
"copyMetadataValuesToRightItem=" + copyToRightItem));
if (copyToItemPermissionCheck(context, relationship, copyToLeftItem, copyToRightItem)) {
// To delete a relationship, a user must have WRITE permissions on one of the related Items
deleteRelationshipAndCopyToItem(context, relationship, copyToLeftItem, copyToRightItem);
} else {
throw new IllegalArgumentException("The relationship given was not valid");
}
}
private void deleteRelationshipAndCopyToItem(Context context, Relationship relationship, boolean copyToLeftItem,
boolean copyToRightItem) throws SQLException, AuthorizeException {
copyMetadataValues(context, relationship, copyToLeftItem, copyToRightItem);
if (authorizeService.authorizeActionBoolean(context, relationship.getLeftItem(), Constants.WRITE) ||
authorizeService.authorizeActionBoolean(context, relationship.getRightItem(), Constants.WRITE)) {
relationshipDAO.delete(context, relationship);
updatePlaceInRelationship(context, relationship, null, null, false, false);
updateItemsInRelationship(context, relationship);
} else {
throw new AuthorizeException(
"You do not have write rights on this relationship's items");
}
}
/**
* Utility method to ensure discovery is updated for the 2 items
* This method is used when creating, modifying or deleting a relationship
* The virtual metadata of the 2 items may need to be updated, so they should be re-indexed
*
* @param context The relevant DSpace context
* @param relationship The relationship which has been created, updated or deleted
* @throws SQLException If something goes wrong
*/
private void updateItemsInRelationship(Context context, Relationship relationship) throws SQLException {
// Since this call is performed after creating, updating or deleting the relationships, the permissions have
// already been verified. The following updateItem calls can however call the
// ItemService.update() functions which would fail if the user doesn't have permission on both items.
// Since we allow this edits to happen under these circumstances, we need to turn off the
// authorization system here so that this failure doesn't happen when the items need to be update
context.turnOffAuthorisationSystem();
try {
// Set a limit on the total amount of items to update at once during a relationship change
int max = configurationService.getIntProperty("relationship.update.relateditems.max", 20);
// Set a limit on the total depth of relationships to traverse during a relationship change
int maxDepth = configurationService.getIntProperty("relationship.update.relateditems.maxdepth", 5);
// This is the list containing all items which will have changes to their virtual metadata
List- itemsToUpdate = new ArrayList<>();
itemsToUpdate.add(relationship.getLeftItem());
itemsToUpdate.add(relationship.getRightItem());
if (containsVirtualMetadata(relationship.getRelationshipType().getLeftwardType())) {
findModifiedDiscoveryItemsForCurrentItem(context, relationship.getLeftItem(),
itemsToUpdate, max, 0, maxDepth);
}
if (containsVirtualMetadata(relationship.getRelationshipType().getRightwardType())) {
findModifiedDiscoveryItemsForCurrentItem(context, relationship.getRightItem(),
itemsToUpdate, max, 0, maxDepth);
}
for (Item item : itemsToUpdate) {
updateItem(context, item);
}
} catch (AuthorizeException e) {
log.error("Authorization Exception while authorization has been disabled", e);
} finally {
context.restoreAuthSystemState();
}
}
/**
* Search for items whose metadata should be updated in discovery and adds them to itemsToUpdate
* It starts from the given item, excludes items already in itemsToUpdate (they're already handled),
* and can be limited in amount of items or depth to update
*/
private void findModifiedDiscoveryItemsForCurrentItem(Context context, Item item, List
- itemsToUpdate,
int max, int currentDepth, int maxDepth)
throws SQLException {
if (itemsToUpdate.size() >= max) {
log.debug("skipping findModifiedDiscoveryItemsForCurrentItem for item "
+ item.getID() + " due to " + itemsToUpdate.size() + " items to be updated");
return;
}
if (currentDepth == maxDepth) {
log.debug("skipping findModifiedDiscoveryItemsForCurrentItem for item "
+ item.getID() + " due to " + currentDepth + " depth");
return;
}
String entityTypeStringFromMetadata = itemService.getEntityTypeLabel(item);
EntityType actualEntityType = entityTypeService.findByEntityType(context, entityTypeStringFromMetadata);
// Get all types of relations for the current item
List
relationshipTypes = relationshipTypeService.findByEntityType(context, actualEntityType);
for (RelationshipType relationshipType : relationshipTypes) {
//are we searching for items where the current item is on the left
boolean isLeft = relationshipType.getLeftType().equals(actualEntityType);
// Verify whether there's virtual metadata configured for this type of relation
// If it's not present, we don't need to update the virtual metadata in discovery
String typeToSearchInVirtualMetadata;
if (isLeft) {
typeToSearchInVirtualMetadata = relationshipType.getRightwardType();
} else {
typeToSearchInVirtualMetadata = relationshipType.getLeftwardType();
}
if (containsVirtualMetadata(typeToSearchInVirtualMetadata)) {
// we have a relationship type where the items attached to the current item will inherit
// virtual metadata from the current item
// retrieving the actual relationships so the related items can be updated
List list = findByItemAndRelationshipType(context, item, relationshipType, isLeft);
for (Relationship foundRelationship : list) {
Item nextItem;
if (isLeft) {
// current item on the left, next item is on the right
nextItem = foundRelationship.getRightItem();
} else {
nextItem = foundRelationship.getLeftItem();
}
// verify it hasn't been processed yet
if (!itemsToUpdate.contains(nextItem)) {
itemsToUpdate.add(nextItem);
// continue the process for the next item, it may also inherit item from the current item
findModifiedDiscoveryItemsForCurrentItem(context, nextItem,
itemsToUpdate, max, currentDepth + 1, maxDepth);
}
}
} else {
log.debug("skipping " + relationshipType.getID()
+ " in findModifiedDiscoveryItemsForCurrentItem for item "
+ item.getID() + " because no relevant virtual metadata was found");
}
}
}
/**
* Verifies whether there is virtual metadata generated for the given relationship
* If no such virtual metadata exists, there's no need to update the items in discovery
* @param typeToSearchInVirtualMetadata a leftWardType or rightWardType of a relationship type
* This can be e.g. isAuthorOfPublication
* @return true if there is virtual metadata for this relationship
*/
private boolean containsVirtualMetadata(String typeToSearchInVirtualMetadata) {
return virtualMetadataPopulator.getMap().containsKey(typeToSearchInVirtualMetadata)
&& virtualMetadataPopulator.getMap().get(typeToSearchInVirtualMetadata).size() > 0;
}
/**
* Converts virtual metadata from RelationshipMetadataValue objects to actual item metadata.
* The resulting MDVs are added in front or behind the Relationship's virtual MDVs.
* The Relationship's virtual MDVs may be shifted right, and all subsequent metadata will be shifted right.
* So this method ensures the places are still valid.
*
* @param context The relevant DSpace context
* @param relationship The relationship containing the left and right items
* @param copyToLeftItem The boolean indicating whether we want to write to left item or not
* @param copyToRightItem The boolean indicating whether we want to write to right item or not
*/
private void copyMetadataValues(Context context, Relationship relationship, boolean copyToLeftItem,
boolean copyToRightItem)
throws SQLException, AuthorizeException {
if (copyToLeftItem) {
String entityTypeString = itemService.getEntityTypeLabel(relationship.getLeftItem());
List relationshipMetadataValues =
relationshipMetadataService.findRelationshipMetadataValueForItemRelationship(context,
relationship.getLeftItem(), entityTypeString, relationship, true);
for (RelationshipMetadataValue relationshipMetadataValue : relationshipMetadataValues) {
// This adds the plain text metadata values on the same spot as the virtual values.
// This will be overruled in org.dspace.content.DSpaceObjectServiceImpl.update
// in the line below but it's not important whether the plain text or virtual values end up on top.
// The virtual values will eventually be deleted, and the others shifted
// This is required because addAndShiftRightMetadata has issues on metadata fields containing
// relationship values which are not useForPlace, while the relationhip type has useForPlace
// E.g. when using addAndShiftRightMetadata on relation.isAuthorOfPublication, it will break the order
// from dc.contributor.author
itemService.addMetadata(context, relationship.getLeftItem(),
relationshipMetadataValue.getMetadataField().
getMetadataSchema().getName(),
relationshipMetadataValue.getMetadataField().getElement(),
relationshipMetadataValue.getMetadataField().getQualifier(),
relationshipMetadataValue.getLanguage(),
relationshipMetadataValue.getValue(), null, -1,
relationshipMetadataValue.getPlace());
}
//This will ensure the new values no longer overlap, but won't break the order
itemService.update(context, relationship.getLeftItem());
}
if (copyToRightItem) {
String entityTypeString = itemService.getEntityTypeLabel(relationship.getRightItem());
List relationshipMetadataValues =
relationshipMetadataService.findRelationshipMetadataValueForItemRelationship(context,
relationship.getRightItem(), entityTypeString, relationship, true);
for (RelationshipMetadataValue relationshipMetadataValue : relationshipMetadataValues) {
itemService.addMetadata(context, relationship.getRightItem(),
relationshipMetadataValue.getMetadataField().
getMetadataSchema().getName(),
relationshipMetadataValue.getMetadataField().getElement(),
relationshipMetadataValue.getMetadataField().getQualifier(),
relationshipMetadataValue.getLanguage(),
relationshipMetadataValue.getValue(), null, -1,
relationshipMetadataValue.getPlace());
}
itemService.update(context, relationship.getRightItem());
}
}
/**
* This method will check if the current user has sufficient rights to write to the respective items if requested
* @param context The relevant DSpace context
* @param relationship The relationship containing the left and right items
* @param copyToLeftItem The boolean indicating whether we want to write to left item or not
* @param copyToRightItem The boolean indicating whether we want to write to right item or not
* @return A boolean indicating whether the permissions are okay for this request
* @throws AuthorizeException If something goes wrong
* @throws SQLException If something goes wrong
*/
private boolean copyToItemPermissionCheck(Context context, Relationship relationship,
boolean copyToLeftItem, boolean copyToRightItem) throws SQLException {
boolean isPermissionCorrect = true;
if (copyToLeftItem) {
if (!authorizeService.authorizeActionBoolean(context, relationship.getLeftItem(), Constants.WRITE)) {
isPermissionCorrect = false;
}
}
if (copyToRightItem) {
if (!authorizeService.authorizeActionBoolean(context, relationship.getRightItem(), Constants.WRITE)) {
isPermissionCorrect = false;
}
}
return isPermissionCorrect;
}
private boolean isRelationshipValidToDelete(Context context, Relationship relationship) throws SQLException {
if (relationship == null) {
log.warn("The relationship has been deemed invalid since the relation was null");
return false;
}
if (relationship.getID() == null) {
log.warn("The relationship has been deemed invalid since the ID" +
" off the given relationship was null");
return false;
}
if (this.find(context, relationship.getID()) == null) {
log.warn("The relationship has been deemed invalid since the relationship" +
" is not present in the DB with the current ID");
logRelationshipTypeDetailsForError(relationship.getRelationshipType());
return false;
}
if (!checkMinCardinality(context, relationship.getLeftItem(),
relationship, relationship.getRelationshipType().getLeftMinCardinality(), true)) {
log.warn("The relationship has been deemed invalid since the leftMinCardinality" +
" constraint would be violated upon deletion");
logRelationshipTypeDetailsForError(relationship.getRelationshipType());
return false;
}
if (!checkMinCardinality(context, relationship.getRightItem(),
relationship, relationship.getRelationshipType().getRightMinCardinality(), false)) {
log.warn("The relationship has been deemed invalid since the rightMinCardinality" +
" constraint would be violated upon deletion");
logRelationshipTypeDetailsForError(relationship.getRelationshipType());
return false;
}
return true;
}
private boolean checkMinCardinality(Context context, Item item,
Relationship relationship,
Integer minCardinality, boolean isLeft) throws SQLException {
List list = this.findByItemAndRelationshipType(context, item, relationship.getRelationshipType(),
isLeft, -1, -1);
if (minCardinality != null && !(list.size() > minCardinality)) {
return false;
}
return true;
}
public List findByItemAndRelationshipType(Context context, Item item,
RelationshipType relationshipType, boolean isLeft)
throws SQLException {
return this.findByItemAndRelationshipType(context, item, relationshipType, isLeft, -1, -1);
}
@Override
public List findByItemAndRelationshipType(Context context, Item item,
RelationshipType relationshipType)
throws SQLException {
return findByItemAndRelationshipType(context, item, relationshipType, -1, -1, true);
}
@Override
public List findByItemAndRelationshipType(Context context, Item item,
RelationshipType relationshipType, int limit, int offset)
throws SQLException {
return findByItemAndRelationshipType(context, item, relationshipType, limit, offset, true);
}
@Override
public List findByItemAndRelationshipType(
Context context, Item item, RelationshipType relationshipType, int limit, int offset, boolean excludeNonLatest
) throws SQLException {
return relationshipDAO
.findByItemAndRelationshipType(context, item, relationshipType, limit, offset, excludeNonLatest);
}
@Override
public List findByItemAndRelationshipType(
Context context, Item item, RelationshipType relationshipType, boolean isLeft, int limit, int offset
) throws SQLException {
return findByItemAndRelationshipType(context, item, relationshipType, isLeft, limit, offset, true);
}
@Override
public List findByItemAndRelationshipType(
Context context, Item item, RelationshipType relationshipType, boolean isLeft, int limit, int offset,
boolean excludeNonLatest
) throws SQLException {
return relationshipDAO
.findByItemAndRelationshipType(context, item, relationshipType, isLeft, limit, offset, excludeNonLatest);
}
@Override
public List findByLatestItemAndRelationshipType(
Context context, Item latestItem, RelationshipType relationshipType, boolean isLeft
) throws SQLException {
return relationshipDAO
.findByLatestItemAndRelationshipType(context, latestItem, relationshipType, isLeft);
}
@Override
public List findByRelationshipType(Context context, RelationshipType relationshipType)
throws SQLException {
return findByRelationshipType(context, relationshipType, -1, -1);
}
@Override
public List findByRelationshipType(Context context, RelationshipType relationshipType, Integer limit,
Integer offset)
throws SQLException {
return relationshipDAO.findByRelationshipType(context, relationshipType, limit, offset);
}
@Override
public List findByTypeName(Context context, String typeName)
throws SQLException {
return this.findByTypeName(context, typeName, -1, -1);
}
@Override
public List findByTypeName(Context context, String typeName, Integer limit, Integer offset)
throws SQLException {
return relationshipDAO.findByTypeName(context, typeName, limit, offset);
}
@Override
public int countTotal(Context context) throws SQLException {
return relationshipDAO.countRows(context);
}
@Override
public int countByItem(Context context, Item item) throws SQLException {
return countByItem(context, item, false, true);
}
@Override
public int countByItem(
Context context, Item item, boolean excludeTilted, boolean excludeNonLatest
) throws SQLException {
return relationshipDAO.countByItem(context, item, excludeTilted, excludeNonLatest);
}
@Override
public int countByRelationshipType(Context context, RelationshipType relationshipType) throws SQLException {
return relationshipDAO.countByRelationshipType(context, relationshipType);
}
@Override
public int countByItemAndRelationshipType(
Context context, Item item, RelationshipType relationshipType, boolean isLeft
) throws SQLException {
return countByItemAndRelationshipType(context, item, relationshipType, isLeft, true);
}
@Override
public int countByItemAndRelationshipType(
Context context, Item item, RelationshipType relationshipType, boolean isLeft, boolean excludeNonLatest
) throws SQLException {
return relationshipDAO
.countByItemAndRelationshipType(context, item, relationshipType, isLeft, excludeNonLatest);
}
@Override
public int countByTypeName(Context context, String typeName)
throws SQLException {
return relationshipDAO.countByTypeName(context, typeName);
}
@Override
public List findByItemRelationshipTypeAndRelatedList(Context context, UUID focusUUID,
RelationshipType relationshipType, List items, boolean isLeft,
int offset, int limit) throws SQLException {
return relationshipDAO
.findByItemAndRelationshipTypeAndList(context, focusUUID, relationshipType, items, isLeft, offset,limit);
}
@Override
public int countByItemRelationshipTypeAndRelatedList(Context context, UUID focusUUID,
RelationshipType relationshipType, List items, boolean isLeft) throws SQLException {
return relationshipDAO
.countByItemAndRelationshipTypeAndList(context, focusUUID, relationshipType, items, isLeft);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy