mirror of
https://github.com/DSpace/DSpace.git
synced 2025-10-17 15:03:18 +00:00
lookup relationship by type, allow entity lookup by meta
This commit is contained in:
@@ -54,6 +54,7 @@ import org.apache.logging.log4j.Logger;
|
|||||||
import org.apache.xpath.XPathAPI;
|
import org.apache.xpath.XPathAPI;
|
||||||
import org.dspace.app.itemimport.service.ItemImportService;
|
import org.dspace.app.itemimport.service.ItemImportService;
|
||||||
import org.dspace.app.util.LocalSchemaFilenameFilter;
|
import org.dspace.app.util.LocalSchemaFilenameFilter;
|
||||||
|
import org.dspace.app.util.RelationshipUtils;
|
||||||
import org.dspace.authorize.AuthorizeException;
|
import org.dspace.authorize.AuthorizeException;
|
||||||
import org.dspace.authorize.ResourcePolicy;
|
import org.dspace.authorize.ResourcePolicy;
|
||||||
import org.dspace.authorize.service.AuthorizeService;
|
import org.dspace.authorize.service.AuthorizeService;
|
||||||
@@ -67,6 +68,7 @@ import org.dspace.content.Item;
|
|||||||
import org.dspace.content.MetadataField;
|
import org.dspace.content.MetadataField;
|
||||||
import org.dspace.content.MetadataSchema;
|
import org.dspace.content.MetadataSchema;
|
||||||
import org.dspace.content.MetadataSchemaEnum;
|
import org.dspace.content.MetadataSchemaEnum;
|
||||||
|
import org.dspace.content.MetadataValue;
|
||||||
import org.dspace.content.Relationship;
|
import org.dspace.content.Relationship;
|
||||||
import org.dspace.content.RelationshipType;
|
import org.dspace.content.RelationshipType;
|
||||||
import org.dspace.content.WorkspaceItem;
|
import org.dspace.content.WorkspaceItem;
|
||||||
@@ -78,6 +80,7 @@ import org.dspace.content.service.InstallItemService;
|
|||||||
import org.dspace.content.service.ItemService;
|
import org.dspace.content.service.ItemService;
|
||||||
import org.dspace.content.service.MetadataFieldService;
|
import org.dspace.content.service.MetadataFieldService;
|
||||||
import org.dspace.content.service.MetadataSchemaService;
|
import org.dspace.content.service.MetadataSchemaService;
|
||||||
|
import org.dspace.content.service.MetadataValueService;
|
||||||
import org.dspace.content.service.RelationshipService;
|
import org.dspace.content.service.RelationshipService;
|
||||||
import org.dspace.content.service.RelationshipTypeService;
|
import org.dspace.content.service.RelationshipTypeService;
|
||||||
import org.dspace.content.service.WorkspaceItemService;
|
import org.dspace.content.service.WorkspaceItemService;
|
||||||
@@ -158,6 +161,8 @@ public class ItemImportServiceImpl implements ItemImportService, InitializingBea
|
|||||||
protected RelationshipService relationshipService;
|
protected RelationshipService relationshipService;
|
||||||
@Autowired(required = true)
|
@Autowired(required = true)
|
||||||
protected RelationshipTypeService relationshipTypeService;
|
protected RelationshipTypeService relationshipTypeService;
|
||||||
|
@Autowired(required = true)
|
||||||
|
protected MetadataValueService metadataValueService;
|
||||||
|
|
||||||
protected String tempWorkDir;
|
protected String tempWorkDir;
|
||||||
|
|
||||||
@@ -168,6 +173,9 @@ public class ItemImportServiceImpl implements ItemImportService, InitializingBea
|
|||||||
protected boolean isQuiet = false;
|
protected boolean isQuiet = false;
|
||||||
protected boolean processRelationships = false;
|
protected boolean processRelationships = false;
|
||||||
|
|
||||||
|
//remember which folder item was imported from
|
||||||
|
Map<String, Item> itemFolderMap = null;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterPropertiesSet() throws Exception {
|
public void afterPropertiesSet() throws Exception {
|
||||||
tempWorkDir = configurationService.getProperty("org.dspace.app.batchitemimport.work.dir");
|
tempWorkDir = configurationService.getProperty("org.dspace.app.batchitemimport.work.dir");
|
||||||
@@ -224,7 +232,7 @@ public class ItemImportServiceImpl implements ItemImportService, InitializingBea
|
|||||||
Map<String, String> skipItems = new HashMap<>(); // set of items to skip if in 'resume'
|
Map<String, String> skipItems = new HashMap<>(); // set of items to skip if in 'resume'
|
||||||
// mode
|
// mode
|
||||||
|
|
||||||
Map<String, Item> itemMap = new HashMap<>(); //remember which folder item was imported from
|
itemFolderMap = new HashMap<>();
|
||||||
|
|
||||||
System.out.println("Adding items from directory: " + sourceDir);
|
System.out.println("Adding items from directory: " + sourceDir);
|
||||||
log.debug("Adding items from directory: " + sourceDir);
|
log.debug("Adding items from directory: " + sourceDir);
|
||||||
@@ -271,7 +279,7 @@ public class ItemImportServiceImpl implements ItemImportService, InitializingBea
|
|||||||
//we still need the item in the map for relationship linking
|
//we still need the item in the map for relationship linking
|
||||||
String skippedHandle = skipItems.get(dircontents[i]);
|
String skippedHandle = skipItems.get(dircontents[i]);
|
||||||
Item skippedItem = (Item) handleService.resolveToObject(c, skippedHandle);
|
Item skippedItem = (Item) handleService.resolveToObject(c, skippedHandle);
|
||||||
itemMap.put(dircontents[i], skippedItem);
|
itemFolderMap.put(dircontents[i], skippedItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -297,7 +305,7 @@ public class ItemImportServiceImpl implements ItemImportService, InitializingBea
|
|||||||
Item item = addItem(c, clist, sourceDir, dircontents[i], mapOut, template);
|
Item item = addItem(c, clist, sourceDir, dircontents[i], mapOut, template);
|
||||||
|
|
||||||
if (processRelationships) {
|
if (processRelationships) {
|
||||||
itemMap.put(dircontents[i], item);
|
itemFolderMap.put(dircontents[i], item);
|
||||||
}
|
}
|
||||||
|
|
||||||
c.uncacheEntity(item);
|
c.uncacheEntity(item);
|
||||||
@@ -307,7 +315,7 @@ public class ItemImportServiceImpl implements ItemImportService, InitializingBea
|
|||||||
|
|
||||||
if (processRelationships) {
|
if (processRelationships) {
|
||||||
//now that all items are imported, iterate again to link relationships
|
//now that all items are imported, iterate again to link relationships
|
||||||
addRelationships(c, sourceDir, itemMap);
|
addRelationships(c, sourceDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
@@ -323,47 +331,75 @@ public class ItemImportServiceImpl implements ItemImportService, InitializingBea
|
|||||||
*
|
*
|
||||||
* @param c Context
|
* @param c Context
|
||||||
* @param sourceDir The parent import source directory
|
* @param sourceDir The parent import source directory
|
||||||
* @param itemMap Item imported in this batch, keyed by their import subfolder
|
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
protected void addRelationships(Context c, String sourceDir, Map<String, Item> itemMap) throws Exception {
|
protected void addRelationships(Context c, String sourceDir) throws Exception {
|
||||||
|
|
||||||
System.out.println("Linking relationships");
|
System.out.println("Linking relationships");
|
||||||
|
|
||||||
for (Map.Entry<String, Item> itemEntry : itemMap.entrySet()) {
|
for (Map.Entry<String, Item> itemEntry : itemFolderMap.entrySet()) {
|
||||||
|
|
||||||
String folderName = itemEntry.getKey();
|
String folderName = itemEntry.getKey();
|
||||||
String path = sourceDir + File.separatorChar + folderName;
|
String path = sourceDir + File.separatorChar + folderName;
|
||||||
Item leftItem = itemEntry.getValue();
|
Item item = itemEntry.getValue();
|
||||||
|
|
||||||
System.out.println("Adding relationships from directory "+ folderName);
|
System.out.println("Adding relationships from directory "+ folderName);
|
||||||
|
|
||||||
//look for a 'relationship' manifest
|
//look for a 'relationship' manifest
|
||||||
Map<Integer, List<String>> relationships = processRelationshipFile(path, "relationships");
|
Map<String, List<String>> relationships = processRelationshipFile(path, "relationships");
|
||||||
if (!relationships.isEmpty()) {
|
if (!relationships.isEmpty()) {
|
||||||
|
|
||||||
for (Map.Entry<Integer, List<String>> relEntry : relationships.entrySet()) {
|
for (Map.Entry<String, List<String>> relEntry : relationships.entrySet()) {
|
||||||
|
|
||||||
Integer relationshipTypeId = relEntry.getKey();
|
String relationshipType = relEntry.getKey();
|
||||||
List<String> identifierList = relEntry.getValue();
|
List<String> identifierList = relEntry.getValue();
|
||||||
|
|
||||||
RelationshipType relationshipType = null;
|
|
||||||
try {
|
|
||||||
relationshipType = relationshipTypeService.find(c, relationshipTypeId.intValue());
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.out.println("\tERROR: relationship type "+ relationshipTypeId +" not found.");
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String itemIdentifier : identifierList) {
|
for (String itemIdentifier : identifierList) {
|
||||||
|
|
||||||
Item rightItem = resolveRelatedItem(c, itemMap, itemIdentifier);
|
//find referenced item
|
||||||
if (null == rightItem) {
|
Item relationItem = resolveRelatedItem(c, itemIdentifier);
|
||||||
throw new Exception("\tERROR: could not find item for "+ itemIdentifier);
|
if (null == relationItem) {
|
||||||
|
throw new Exception("Could not find item for "+ itemIdentifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
Relationship relationship = relationshipService.create(c, leftItem, rightItem, relationshipType, -1, -1);
|
//get entity type of entity and item
|
||||||
System.out.println("\tAdded relationship (type: "+ relationshipTypeId +") to "+ rightItem.getHandle());
|
String itemEntityType = getEntityType(item);
|
||||||
|
String relatedEntityType = getEntityType(relationItem);
|
||||||
|
|
||||||
|
//find matching relationship type
|
||||||
|
List<RelationshipType> relTypes = relationshipTypeService.findByLeftwardOrRightwardTypeName(c, relationshipType);
|
||||||
|
RelationshipType foundRelationshipType = RelationshipUtils.matchRelationshipType(relTypes, relatedEntityType, itemEntityType, relationshipType);
|
||||||
|
if (foundRelationshipType == null) {
|
||||||
|
throw new Exception("No Relationship type found for:\n" +
|
||||||
|
"Target type: " + relatedEntityType + "\n" +
|
||||||
|
"Origin referer type: " + itemEntityType + "\n" +
|
||||||
|
"with typeName: " + relationshipType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean left = false;
|
||||||
|
if (foundRelationshipType.getLeftwardType().equalsIgnoreCase(relationshipType)) {
|
||||||
|
left = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder items for relation placing
|
||||||
|
Item leftItem = null;
|
||||||
|
Item rightItem = null;
|
||||||
|
if (left) {
|
||||||
|
leftItem = item;
|
||||||
|
rightItem = relationItem;
|
||||||
|
} else {
|
||||||
|
leftItem = relationItem;
|
||||||
|
rightItem = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the relationship
|
||||||
|
int leftPlace = relationshipService.findNextLeftPlaceByLeftItem(c, leftItem);
|
||||||
|
int rightPlace = relationshipService.findNextRightPlaceByRightItem(c, rightItem);
|
||||||
|
Relationship persistedRelationship = relationshipService.create(c, leftItem, rightItem, foundRelationshipType, leftPlace, rightPlace);
|
||||||
|
relationshipService.update(c, persistedRelationship);
|
||||||
|
|
||||||
|
System.out.println("\tAdded relationship (type: "+ foundRelationshipType.getID() +") from "+ leftItem.getHandle() +" to "+ rightItem.getHandle());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,12 +411,22 @@ public class ItemImportServiceImpl implements ItemImportService, InitializingBea
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the item's entity type from meta.
|
||||||
|
*
|
||||||
|
* @param item
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
protected String getEntityType(Item item) throws Exception {
|
||||||
|
return itemService.getMetadata(item, "dspace", "entity", "type", Item.ANY).get(0).getValue();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read the relationship manifest file.
|
* Read the relationship manifest file.
|
||||||
*
|
*
|
||||||
* Each line in the file contains a relationship type id and an item identifier in the following format:
|
* Each line in the file contains a relationship type id and an item identifier in the following format:
|
||||||
*
|
*
|
||||||
* <relationship_type_id> <handle|uuid|import_item_folder>
|
* relation.<relation_key> <handle|uuid|folderName:import_item_folder|schema.element[.qualifier]:value>
|
||||||
*
|
*
|
||||||
* The input_item_folder should refer the folder name of another item in this import batch.
|
* The input_item_folder should refer the folder name of another item in this import batch.
|
||||||
*
|
*
|
||||||
@@ -389,10 +435,10 @@ public class ItemImportServiceImpl implements ItemImportService, InitializingBea
|
|||||||
* @return Map of found relationships
|
* @return Map of found relationships
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
protected Map<Integer, List<String>> processRelationshipFile(String path, String filename) throws Exception {
|
protected Map<String, List<String>> processRelationshipFile(String path, String filename) throws Exception {
|
||||||
|
|
||||||
File file = new File(path + File.separatorChar + filename);
|
File file = new File(path + File.separatorChar + filename);
|
||||||
Map<Integer, List<String>> result = new HashMap<>();
|
Map<String, List<String>> result = new HashMap<>();
|
||||||
|
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
|
|
||||||
@@ -408,33 +454,31 @@ public class ItemImportServiceImpl implements ItemImportService, InitializingBea
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
int relationshipTypeId;
|
String relationshipType = null;
|
||||||
String itemIdentifier = null;
|
String itemIdentifier = null;
|
||||||
|
|
||||||
//format: <relationship_id> <related_folder_name>
|
|
||||||
StringTokenizer st = new StringTokenizer(line);
|
StringTokenizer st = new StringTokenizer(line);
|
||||||
|
|
||||||
if (st.hasMoreTokens()) {
|
if (st.hasMoreTokens()) {
|
||||||
try {
|
relationshipType= st.nextToken();
|
||||||
relationshipTypeId = Integer.valueOf(st.nextToken());
|
if (relationshipType.split("\\.").length > 1) {
|
||||||
} catch (NumberFormatException e) {
|
relationshipType = relationshipType.split("\\.")[1];
|
||||||
throw new Exception("Bad mapfile line:\n" + line);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Exception("Bad mapfile line:\n" + line);
|
throw new Exception("Bad mapfile line:\n" + line);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (st.hasMoreTokens()) {
|
if (st.hasMoreTokens()) {
|
||||||
itemIdentifier = st.nextToken();
|
itemIdentifier = st.nextToken("").trim();
|
||||||
} else {
|
} else {
|
||||||
throw new Exception("Bad mapfile line:\n" + line);
|
throw new Exception("Bad mapfile line:\n" + line);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.containsKey(relationshipTypeId)) {
|
if (!result.containsKey(relationshipType)) {
|
||||||
result.put(relationshipTypeId, new ArrayList<>());
|
result.put(relationshipType, new ArrayList<>());
|
||||||
}
|
}
|
||||||
|
|
||||||
result.get(relationshipTypeId).add(itemIdentifier);
|
result.get(relationshipType).add(itemIdentifier);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,26 +508,81 @@ public class ItemImportServiceImpl implements ItemImportService, InitializingBea
|
|||||||
* that was just imported. Next it will try to find the item by handle or UUID.
|
* that was just imported. Next it will try to find the item by handle or UUID.
|
||||||
*
|
*
|
||||||
* @param c Context
|
* @param c Context
|
||||||
* @param itemMap Item imported in this batch, keyed by their import subfolder
|
|
||||||
* @param itemIdentifier The identifier string found in the import manifest (handle, uuid, or another import subfolder)
|
* @param itemIdentifier The identifier string found in the import manifest (handle, uuid, or another import subfolder)
|
||||||
* @return Item if found, or null.
|
* @return Item if found, or null.
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
protected Item resolveRelatedItem(Context c, Map<String, Item> itemMap, String itemIdentifier) throws Exception {
|
protected Item resolveRelatedItem(Context c, String itemIdentifier) throws Exception {
|
||||||
|
|
||||||
Item item = null;
|
if (itemIdentifier.contains(":")) {
|
||||||
|
|
||||||
if (itemMap.containsKey(itemIdentifier)) {
|
if (itemIdentifier.startsWith("folderName:") || itemIdentifier.startsWith("rowName:")) {
|
||||||
//identifier refers to a folder name in this import batch
|
//identifier refers to a folder name in this import
|
||||||
item = itemMap.get(itemIdentifier);
|
int i = itemIdentifier.indexOf(":");
|
||||||
|
String folderName = itemIdentifier.substring(i + 1);
|
||||||
|
if (itemFolderMap.containsKey(folderName)) {
|
||||||
|
return itemFolderMap.get(folderName);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
//lookup by meta value
|
||||||
|
int i = itemIdentifier.indexOf(":");
|
||||||
|
String metaKey = itemIdentifier.substring(0, i);
|
||||||
|
String metaValue = itemIdentifier.substring(i + 1);
|
||||||
|
return findItemByMetaValue(c, metaKey, metaValue);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
} else if (itemIdentifier.indexOf('/') != -1) {
|
} else if (itemIdentifier.indexOf('/') != -1) {
|
||||||
//resolve by handle
|
//resolve by handle
|
||||||
item = (Item) handleService.resolveToObject(c, itemIdentifier);
|
return (Item) handleService.resolveToObject(c, itemIdentifier);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
//try to resolve by UUID
|
//try to resolve by UUID
|
||||||
item = itemService.findByIdOrLegacyId(c, itemIdentifier);
|
return itemService.findByIdOrLegacyId(c, itemIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lookup an item by a (unique) meta value.
|
||||||
|
*
|
||||||
|
* @param metaKey
|
||||||
|
* @param metaValue
|
||||||
|
* @return Item
|
||||||
|
* @throws Exception if single item not found.
|
||||||
|
*/
|
||||||
|
protected Item findItemByMetaValue(Context c, String metaKey, String metaValue) throws Exception {
|
||||||
|
|
||||||
|
Item item = null;
|
||||||
|
|
||||||
|
String mf[] = metaKey.split("\\.");
|
||||||
|
if (mf.length < 2) {
|
||||||
|
throw new Exception("Bad metadata field in reference: '" + metaKey + "' (expected syntax is schema.element[.qualifier])");
|
||||||
|
}
|
||||||
|
String schema = mf[0];
|
||||||
|
String element = mf[1];
|
||||||
|
String qualifier = mf.length == 2 ? null : mf[2];
|
||||||
|
try {
|
||||||
|
MetadataField mfo = metadataFieldService.findByElement(c, schema, element, qualifier);
|
||||||
|
Iterator<MetadataValue> mdv = metadataValueService.findByFieldAndValue(c, mfo, metaValue);
|
||||||
|
if (mdv.hasNext()) {
|
||||||
|
MetadataValue mdvVal = mdv.next();
|
||||||
|
UUID uuid = mdvVal.getDSpaceObject().getID();
|
||||||
|
if (mdv.hasNext()) {
|
||||||
|
throw new Exception("Ambiguous reference; multiple matches in db: " + metaKey);
|
||||||
|
}
|
||||||
|
item = itemService.find(c, uuid);
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new Exception("Error looking up item by metadata reference: " + metaKey, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item == null) {
|
||||||
|
throw new Exception("Item not found by metadata reference: " + metaKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
|
@@ -0,0 +1,50 @@
|
|||||||
|
package org.dspace.app.util;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.dspace.content.RelationshipType;
|
||||||
|
|
||||||
|
public class RelationshipUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches two Entity types to a Relationship Type from a set of Relationship Types.
|
||||||
|
*
|
||||||
|
* @param relTypes set of Relationship Types.
|
||||||
|
* @param targetType entity type of target.
|
||||||
|
* @param originType entity type of origin referer.
|
||||||
|
* @return null or matched Relationship Type.
|
||||||
|
*/
|
||||||
|
public static RelationshipType matchRelationshipType(List<RelationshipType> relTypes, String targetType, String originType, String originTypeName) {
|
||||||
|
RelationshipType foundRelationshipType = null;
|
||||||
|
if (originTypeName.split("\\.").length > 1) {
|
||||||
|
originTypeName = originTypeName.split("\\.")[1];
|
||||||
|
}
|
||||||
|
for (RelationshipType relationshipType : relTypes) {
|
||||||
|
// Is origin type leftward or righward
|
||||||
|
boolean isLeft = false;
|
||||||
|
if (relationshipType.getLeftType().getLabel().equalsIgnoreCase(originType)) {
|
||||||
|
isLeft = true;
|
||||||
|
}
|
||||||
|
if (isLeft) {
|
||||||
|
// Validate typeName reference
|
||||||
|
if (!relationshipType.getLeftwardType().equalsIgnoreCase(originTypeName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (relationshipType.getLeftType().getLabel().equalsIgnoreCase(originType) &&
|
||||||
|
relationshipType.getRightType().getLabel().equalsIgnoreCase(targetType)) {
|
||||||
|
foundRelationshipType = relationshipType;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!relationshipType.getRightwardType().equalsIgnoreCase(originTypeName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (relationshipType.getLeftType().getLabel().equalsIgnoreCase(targetType) &&
|
||||||
|
relationshipType.getRightType().getLabel().equalsIgnoreCase(originType)) {
|
||||||
|
foundRelationshipType = relationshipType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return foundRelationshipType;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Reference in New Issue
Block a user