/**
* 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 org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.authority.Choices;
import org.dspace.content.authority.service.ChoiceAuthorityService;
import org.dspace.content.authority.service.MetadataAuthorityService;
import org.dspace.content.service.DSpaceObjectService;
import org.dspace.content.service.MetadataFieldService;
import org.dspace.content.service.MetadataValueService;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.handle.service.HandleService;
import org.dspace.identifier.service.IdentifierService;
import org.dspace.utils.DSpace;
import org.springframework.beans.factory.annotation.Autowired;
import java.sql.SQLException;
import java.util.*;
/**
* Service implementation class for the DSpaceObject.
* All DSpaceObject service classes should extend this class since it implements some basic methods which all DSpaceObjects
* are required to have.
*
* @author kevinvandevelde at atmire.com
* @param <T> class type
*/
public abstract class DSpaceObjectServiceImpl<T extends DSpaceObject> implements DSpaceObjectService<T> {
/** log4j category */
private static final Logger log = Logger.getLogger(DSpaceObjectServiceImpl.class);
@Autowired(required = true)
protected ChoiceAuthorityService choiceAuthorityService;
@Autowired(required = true)
protected HandleService handleService;
@Autowired(required = true)
protected MetadataValueService metadataValueService;
@Autowired(required = true)
protected MetadataFieldService metadataFieldService;
@Autowired(required = true)
protected MetadataAuthorityService metadataAuthorityService;
public DSpaceObjectServiceImpl()
{
}
@Override
public String getName(T dso) {
String value = getMetadataFirstValue(dso, MetadataSchema.DC_SCHEMA, "title", null, Item.ANY);
return value == null ? "" : value;
}
@Override
public ArrayList<String> getIdentifiers(Context context, T dso) {
ArrayList<String > identifiers = new ArrayList<>();
IdentifierService identifierService =
new DSpace().getSingletonService(IdentifierService.class);
if (identifierService != null)
{
identifiers.addAll(identifierService.lookup(context, dso));
} else {
log.warn("No IdentifierService found, will return an list containing "
+ "the Handle only.");
if (dso.getHandle() != null)
{
identifiers.add(handleService.getCanonicalForm(dso.getHandle()));
}
}
if (log.isDebugEnabled())
{
StringBuilder dbgMsg = new StringBuilder();
for (String id : identifiers)
{
if (dbgMsg.capacity() == 0)
{
dbgMsg.append("This DSO's Identifiers are: ");
} else {
dbgMsg.append(", ");
}
dbgMsg.append(id);
}
dbgMsg.append(".");
log.debug(dbgMsg.toString());
}
return identifiers;
}
@Override
public DSpaceObject getParentObject(Context context, T dso) throws SQLException
{
return null;
}
@Override
public DSpaceObject getAdminObject(Context context, T dso, int action) throws SQLException {
if (action == Constants.ADMIN)
{
throw new IllegalArgumentException("Illegal call to the DSpaceObject.getAdminObject method");
}
return dso;
}
@Override
public String getTypeText(T dso) {
return Constants.typeText[dso.getType()];
}
@Override
public List<MetadataValue> getMetadata(T dso, String schema, String element, String qualifier, String lang) {
// Build up list of matching values
List<MetadataValue> values = new ArrayList<MetadataValue>();
for (MetadataValue dcv : dso.getMetadata())
{
if (match(schema, element, qualifier, lang, dcv))
{
values.add(dcv);
}
}
// Create an array of matching values
return values;
}
@Override
public List<MetadataValue> getMetadataByMetadataString(T dso, String mdString)
{
StringTokenizer dcf = new StringTokenizer(mdString, ".");
String[] tokens = { "", "", "" };
int i = 0;
while(dcf.hasMoreTokens())
{
tokens[i] = dcf.nextToken().trim();
i++;
}
String schema = tokens[0];
String element = tokens[1];
String qualifier = tokens[2];
List<MetadataValue> values;
if (Item.ANY.equals(qualifier))
{
values = getMetadata(dso, schema, element, Item.ANY, Item.ANY);
}
else if ("".equals(qualifier))
{
values = getMetadata(dso, schema, element, null, Item.ANY);
}
else
{
values = getMetadata(dso, schema, element, qualifier, Item.ANY);
}
return values;
}
@Override
public String getMetadata(T dso, String value) {
List<MetadataValue> metadataValues = getMetadataByMetadataString(dso, value);
if(CollectionUtils.isNotEmpty(metadataValues)) {
return metadataValues.iterator().next().getValue();
}
return null;
}
@Override
public List<MetadataValue> getMetadata(T dso, String mdString, String authority) {
String[] elements = getElements(mdString);
return getMetadata(dso, elements[0], elements[1], elements[2], elements[3], authority);
}
@Override
public List<MetadataValue> getMetadata(T dso, String schema, String element, String qualifier, String lang, String authority){
List<MetadataValue> metadata = getMetadata(dso, schema, element, qualifier, lang);
List<MetadataValue> result = new ArrayList<>(metadata);
if (!authority.equals(Item.ANY)) {
Iterator<MetadataValue> iterator = result.iterator();
while (iterator.hasNext()) {
MetadataValue metadataValue = iterator.next();
if (!authority.equals(metadataValue.getAuthority())) {
iterator.remove();
}
}
}
return result;
}
@Override
public void addMetadata(Context context, T dso, String schema, String element, String qualifier, String lang, List<String> values) throws SQLException {
MetadataField metadataField = metadataFieldService.findByElement(context, schema, element, qualifier);
if (metadataField == null) {
throw new SQLException("bad_dublin_core schema=" + schema + "." + element + "." + qualifier + ". Metadata field does not exist!");
}
addMetadata(context, dso, metadataField, lang, values);
}
@Override
public void addMetadata(Context context, T dso, String schema, String element, String qualifier, String lang, List<String> values, List<String> authorities, List<Integer> confidences) throws SQLException {
// We will not verify that they are valid entries in the registry
// until update() is called.
MetadataField metadataField = metadataFieldService.findByElement(context, schema, element, qualifier);
if (metadataField == null) {
throw new SQLException("bad_dublin_core schema=" + schema + "." + element + "." + qualifier + ". Metadata field does not exist!");
}
addMetadata(context, dso, metadataField, lang, values, authorities, confidences);
}
@Override
public void addMetadata(Context context, T dso, MetadataField metadataField, String lang, List<String> values, List<String> authorities, List<Integer> confidences) throws SQLException {
boolean authorityControlled = metadataAuthorityService.isAuthorityControlled(metadataField);
boolean authorityRequired = metadataAuthorityService.isAuthorityRequired(metadataField);
// We will not verify that they are valid entries in the registry
// until update() is called.
for (int i = 0; i < values.size(); i++)
{
MetadataValue metadataValue = metadataValueService.create(context, dso, metadataField);
metadataValue.setLanguage(lang == null ? null : lang.trim());
// Logic to set Authority and Confidence:
// - normalize an empty string for authority to NULL.
// - if authority key is present, use given confidence or NOVALUE if not given
// - otherwise, preserve confidence if meaningful value was given since it may document a failed authority lookup
// - CF_UNSET signifies no authority nor meaningful confidence.
// - it's possible to have empty authority & CF_ACCEPTED if e.g. user deletes authority key
if (authorityControlled)
{
if (authorities != null && authorities.get(i) != null && authorities.get(i).length() > 0)
{
metadataValue.setAuthority(authorities.get(i));
metadataValue.setConfidence(confidences == null ? Choices.CF_NOVALUE : confidences.get(i));
}
else
{
metadataValue.setAuthority(null);
metadataValue.setConfidence(confidences == null ? Choices.CF_UNSET : confidences.get(i));
}
// authority sanity check: if authority is required, was it supplied?
// XXX FIXME? can't throw a "real" exception here without changing all the callers to expect it, so use a runtime exception
if (authorityRequired && (metadataValue.getAuthority() == null || metadataValue.getAuthority().length() == 0))
{
throw new IllegalArgumentException("The metadata field \"" + metadataField.toString() + "\" requires an authority key but none was provided. Value=\"" + values.get(i) + "\"");
}
}
if (values.get(i) != null)
{
// remove control unicode char
String temp = values.get(i).trim();
char[] dcvalue = temp.toCharArray();
for (int charPos = 0; charPos < dcvalue.length; charPos++)
{
if (Character.isISOControl(dcvalue[charPos]) &&
!String.valueOf(dcvalue[charPos]).equals("\u0009") &&
!String.valueOf(dcvalue[charPos]).equals("\n") &&
!String.valueOf(dcvalue[charPos]).equals("\r"))
{
dcvalue[charPos] = ' ';
}
}
metadataValue.setValue(String.valueOf(dcvalue));;
}
else
{
metadataValue.setValue(null);
}
//An update here isn't needed, this is persited upon the merge of the owning object
// metadataValueService.update(context, metadataValue);
dso.addDetails(metadataField.toString());
}
}
@Override
public void addMetadata(Context context, T dso, MetadataField metadataField, String language, String value, String authority, int confidence) throws SQLException {
addMetadata(context, dso, metadataField, language, Arrays.asList(value), Arrays.asList(authority), Arrays.asList(confidence));
}
@Override
public void addMetadata(Context context, T dso, String schema, String element, String qualifier, String lang, String value) throws SQLException {
addMetadata(context, dso, schema, element, qualifier, lang, Arrays.asList(value));
}
@Override
public void addMetadata(Context context, T dso, MetadataField metadataField, String language, String value) throws SQLException {
addMetadata(context, dso, metadataField, language, Arrays.asList(value));
}
@Override
public void addMetadata(Context context, T dso, MetadataField metadataField, String language, List<String> values) throws SQLException {
if(metadataField != null) {
String fieldKey = metadataAuthorityService.makeFieldKey(metadataField.getMetadataSchema().getName(), metadataField.getElement(), metadataField.getQualifier());
if (metadataAuthorityService.isAuthorityControlled(fieldKey)) {
List<String> authorities = new ArrayList<String>();
List<Integer> confidences = new ArrayList<Integer>();
for (int i = 0; i < values.size(); ++i) {
if (dso instanceof Item) {
getAuthoritiesAndConfidences(fieldKey, ((Item) dso).getOwningCollection(), values, authorities, confidences, i);
} else {
getAuthoritiesAndConfidences(fieldKey, null, values, authorities, confidences, i);
}
}
addMetadata(context, dso, metadataField, language, values, authorities, confidences);
} else {
addMetadata(context, dso, metadataField, language, values, null, null);
}
}
}
@Override
public void addMetadata(Context context, T dso, String schema, String element, String qualifier, String lang, String value, String authority, int confidence) throws SQLException {
addMetadata(context, dso, schema, element, qualifier, lang, Arrays.asList(value), Arrays.asList(authority), Arrays.asList(confidence));
}
@Override
public void clearMetadata(Context context, T dso, String schema, String element, String qualifier, String lang) throws SQLException {
Iterator<MetadataValue> metadata = dso.getMetadata().iterator();
while (metadata.hasNext())
{
MetadataValue metadataValue = metadata.next();
// If this value matches, delete it
if (match(schema, element, qualifier, lang, metadataValue))
{
metadata.remove();
metadataValueService.delete(context, metadataValue);
}
}
dso.setMetadataModified();
}
@Override
public void removeMetadataValues(Context context, T dso, List<MetadataValue> values) throws SQLException {
Iterator<MetadataValue> metadata = dso.getMetadata().iterator();
while (metadata.hasNext()) {
MetadataValue metadataValue = metadata.next();
if(values.contains(metadataValue))
{
metadata.remove();
metadataValueService.delete(context, metadataValue);
}
}
dso.setMetadataModified();
}
/**
* Retrieve first metadata field value
* @param dso
* The DSpaceObject which we ask for metadata.
* @param schema
* the schema for the metadata field. <em>Must</em> match
* the <code>name</code> of an existing metadata schema.
* @param element
* the element to match, or <code>Item.ANY</code>
* @param qualifier
* the qualifier to match, or <code>Item.ANY</code>
* @param language
* the language to match, or <code>Item.ANY</code>
* @return the first metadata field value
*/
@Override
public String getMetadataFirstValue(T dso, String schema, String element, String qualifier, String language){
List<MetadataValue> metadataValues = getMetadata(dso, schema, element, qualifier, language);
if(CollectionUtils.isNotEmpty(metadataValues)){
return metadataValues.iterator().next().getValue();
}
return null;
}
/**
* Set first metadata field value
* @throws SQLException if database error
*/
@Override
public void setMetadataSingleValue(Context context, T dso, String schema, String element, String qualifier, String language, String value) throws SQLException {
if(value != null)
{
clearMetadata(context, dso, schema, element, qualifier, language);
addMetadata(context, dso, schema, element, qualifier, language, value);
dso.setMetadataModified();
}
}
/**
* Utility method for pattern-matching metadata elements. This
* method will return <code>true</code> if the given schema,
* element, qualifier and language match the schema, element,
* qualifier and language of the <code>DCValue</code> object passed
* in. Any or all of the element, qualifier and language passed
* in can be the <code>Item.ANY</code> wildcard.
*
* @param schema
* the schema for the metadata field. <em>Must</em> match
* the <code>name</code> of an existing metadata schema.
* @param element
* the element to match, or <code>Item.ANY</code>
* @param qualifier
* the qualifier to match, or <code>Item.ANY</code>
* @param language
* the language to match, or <code>Item.ANY</code>
* @param metadataValue
* the Dublin Core value
* @return <code>true</code> if there is a match
*/
protected boolean match(String schema, String element, String qualifier,
String language, MetadataValue metadataValue)
{
MetadataField metadataField = metadataValue.getMetadataField();
MetadataSchema metadataSchema = metadataField.getMetadataSchema();
// We will attempt to disprove a match - if we can't we have a match
if (!element.equals(Item.ANY) && !element.equals(metadataField.getElement()))
{
// Elements do not match, no wildcard
return false;
}
if (qualifier == null)
{
// Value must be unqualified
if (metadataField.getQualifier() != null)
{
// Value is qualified, so no match
return false;
}
}
else if (!qualifier.equals(Item.ANY))
{
// Not a wildcard, so qualifier must match exactly
if (!qualifier.equals(metadataField.getQualifier()))
{
return false;
}
}
if (language == null)
{
// Value must be null language to match
if (metadataValue.getLanguage() != null)
{
// Value is qualified, so no match
return false;
}
}
else if (!language.equals(Item.ANY))
{
// Not a wildcard, so language must match exactly
if (!language.equals(metadataValue.getLanguage()))
{
return false;
}
}
if (!schema.equals(Item.ANY))
{
if (metadataSchema != null && !metadataSchema.getName().equals(schema))
{
// The namespace doesn't match
return false;
}
}
// If we get this far, we have a match
return true;
}
protected void getAuthoritiesAndConfidences(String fieldKey, Collection collection, List<String> values, List<String> authorities, List<Integer> confidences, int i) {
Choices c = choiceAuthorityService.getBestMatch(fieldKey, values.get(i), null, null);
authorities.add(c.values.length > 0 ? c.values[0].authority : null);
confidences.add(c.confidence);
}
/**
* Splits "schema.element.qualifier.language" into an array.
* <p>
* The returned array will always have length greater than or equal to 4
* <p>
* Values in the returned array can be empty or null.
* @param fieldName field name
* @return array
*/
protected String[] getElements(String fieldName) {
String[] tokens = StringUtils.split(fieldName, ".");
int add = 4 - tokens.length;
if (add > 0) {
tokens = (String[]) ArrayUtils.addAll(tokens, new String[add]);
}
return tokens;
}
/**
* Splits "schema.element.qualifier.language" into an array.
* <p>
* The returned array will always have length greater than or equal to 4
* <p>
* When @param fill is true, elements that would be empty or null are replaced by Item.ANY
* @param fieldName field name
* @return array
*/
protected String[] getElementsFilled(String fieldName) {
String[] elements = getElements(fieldName);
for (int i = 0; i < elements.length; i++) {
if (StringUtils.isBlank(elements[i])) {
elements[i] = Item.ANY;
}
}
return elements;
}
protected String[] getMDValueByField(String field){
StringTokenizer dcf = new StringTokenizer(field, ".");
String[] tokens = { "", "", "" };
int i = 0;
while(dcf.hasMoreTokens()){
tokens[i] = dcf.nextToken().trim();
i++;
}
if(i!=0){
return tokens;
}else{
return getMDValueByLegacyField(field);
}
}
@Override
public void update(Context context, T dso) throws SQLException, AuthorizeException
{
if(dso.isMetadataModified())
{
/*
Update the order of the metadata values
*/
// A map created to store the latest place for each metadata field
Map<MetadataField, Integer> fieldToLastPlace = new HashMap<>();
List<MetadataValue> metadataValues = dso.getMetadata();
for (MetadataValue metadataValue : metadataValues)
{
//Retrieve & store the place for each metadata value
int mvPlace = getMetadataValuePlace(fieldToLastPlace, metadataValue);
metadataValue.setPlace(mvPlace);
}
}
}
/**
* Retrieve the place of the metadata value
* @param fieldToLastPlace the map containing the latest place of each metadata field
* @param metadataValue the metadata value that needs to get a place
* @return The new place for the metadata valu
*/
protected int getMetadataValuePlace(Map<MetadataField, Integer> fieldToLastPlace, MetadataValue metadataValue) {
MetadataField metadataField = metadataValue.getMetadataField();
if(fieldToLastPlace.containsKey(metadataField))
{
fieldToLastPlace.put(metadataField, fieldToLastPlace.get(metadataField) + 1);
}else{
// The metadata value place starts at 0
fieldToLastPlace.put(metadataField, 0);
}
return fieldToLastPlace.get(metadataField);
}
protected String[] getMDValueByLegacyField(String field){
switch (field) {
case "introductory_text":
return new String[]{MetadataSchema.DC_SCHEMA, "description", null};
case "short_description":
return new String[]{MetadataSchema.DC_SCHEMA, "description", "abstract"};
case "side_bar_text":
return new String[]{MetadataSchema.DC_SCHEMA, "description", "tableofcontents"};
case "copyright_text":
return new String[]{MetadataSchema.DC_SCHEMA, "rights", null};
case "name":
return new String[]{MetadataSchema.DC_SCHEMA, "title", null};
case "provenance_description":
return new String[]{MetadataSchema.DC_SCHEMA,"provenance", null};
case "license":
return new String[]{MetadataSchema.DC_SCHEMA, "rights", "license"};
case "user_format_description":
return new String[]{MetadataSchema.DC_SCHEMA,"format",null};
case "source":
return new String[]{MetadataSchema.DC_SCHEMA,"source",null};
case "firstname":
return new String[]{"eperson","firstname",null};
case "lastname":
return new String[]{"eperson","lastname",null};
case "phone":
return new String[]{"eperson","phone",null};
case "language":
return new String[]{"eperson","language",null};
default:
return new String[]{null, null, null};
}
}
}