diff --git a/dspace-api/src/main/java/org/dspace/content/authority/Choice.java b/dspace-api/src/main/java/org/dspace/content/authority/Choice.java index 9b68c75d28..6d73bdb5ea 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/Choice.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/Choice.java @@ -33,21 +33,62 @@ public class Choice { */ public String value = null; + /** + * A boolean representing if choice entry value can selected (usually true). + * Hierarchical authority can flag some choice as not selectable to force the + * use to choice a more detailed terms in the tree, such a leaf or a deeper + * branch + */ + public boolean selectable = true; + public Map extras = new HashMap(); public Choice() { } + /** + * Minimal constructor for this data object. It assumes an empty map of extras + * information and a selected choice + * + * @param authority the authority key + * @param value the text value to store in the metadata + * @param label the value to display to the user + */ public Choice(String authority, String value, String label) { this.authority = authority; this.value = value; this.label = label; } + /** + * Constructor to quickly setup the data object for basic authorities. The choice is assumed to be selectable. + * + * @param authority the authority key + * @param value the text value to store in the metadata + * @param label the value to display to the user + * @param extras a key value map of extra information related to this choice + */ public Choice(String authority, String label, String value, Map extras) { this.authority = authority; this.label = label; this.value = value; this.extras = extras; } + + /** + * Constructor for common need of Hierarchical authorities that want to + * explicitely set the selectable flag + * + * @param authority the authority key + * @param value the text value to store in the metadata + * @param label the value to display to the user + * @param selectable true if the choice can be selected, false if the a more + * accurate choice should be preferred + */ + public Choice(String authority, String label, String value, boolean selectable) { + this.authority = authority; + this.label = label; + this.value = value; + this.selectable = selectable; + } } diff --git a/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthority.java b/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthority.java index d2d06fe983..750e761f3d 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthority.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthority.java @@ -7,7 +7,10 @@ */ package org.dspace.content.authority; -import org.dspace.content.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.dspace.core.NameAwarePlugin; /** * Plugin interface that supplies an authority control mechanism for @@ -17,7 +20,7 @@ import org.dspace.content.Collection; * @see ChoiceAuthorityServiceImpl * @see MetadataAuthorityServiceImpl */ -public interface ChoiceAuthority { +public interface ChoiceAuthority extends NameAwarePlugin { /** * Get all values from the authority that match the preferred value. * Note that the offering was entered by the user and may contain @@ -32,15 +35,13 @@ public interface ChoiceAuthority { * defaultSelected index in the Choices instance to the choice, if any, * that matches the value. * - * @param field being matched for * @param text user's value to match - * @param collection database ID of Collection for context (owner of Item) * @param start choice at which to start, 0 is first. * @param limit maximum number of choices to return, 0 for no limit. * @param locale explicit localization key if available, or null * @return a Choices object (never null). */ - public Choices getMatches(String field, String text, Collection collection, int start, int limit, String locale); + public Choices getMatches(String text, int start, int limit, String locale); /** * Get the single "best" match (if any) of a value in the authority @@ -51,13 +52,11 @@ public interface ChoiceAuthority { * This call is typically used in non-interactive metadata ingest * where there is no interactive agent to choose from among options. * - * @param field being matched for * @param text user's value to match - * @param collection database ID of Collection for context (owner of Item) * @param locale explicit localization key if available, or null * @return a Choices object (never null) with 1 or 0 values. */ - public Choices getBestMatch(String field, String text, Collection collection, String locale); + public Choices getBestMatch(String text, String locale); /** * Get the canonical user-visible "label" (i.e. short descriptive text) @@ -67,31 +66,97 @@ public interface ChoiceAuthority { * This may get called many times while populating a Web page so it should * be implemented as efficiently as possible. * - * @param field being matched for * @param key authority key known to this authority. * @param locale explicit localization key if available, or null * @return descriptive label - should always return something, never null. */ - public String getLabel(String field, String key, String locale); + public String getLabel(String key, String locale); + /** + * Get the canonical value to store for a key in the authority. Can be localized + * given the implicit or explicit locale specification. + * + * @param key authority key known to this authority. + * @param locale explicit localization key if available, or null + * @return value to store - should always return something, never null. + */ + default String getValue(String key, String locale) { + return getLabel(key, locale); + } + + /** + * Get a map of additional information related to the specified key in the + * authority. + * + * @param key the key of the entry + * @param locale explicit localization key if available, or null + * @return a map of additional information related to the key + */ + default Map getExtra(String key, String locale) { + return new HashMap(); + } + + /** + * Return true for hierarchical authorities + * + * @return true if hierarchical, default false + */ default boolean isHierarchical() { return false; } + /** + * Scrollable authorities allows the scroll of the entries without applying + * filter/query to the + * {@link #getMatches(String, String, Collection, int, int, String)} + * + * @return true if scrollable, default false + */ default boolean isScrollable() { return false; } - default boolean hasIdentifier() { - return true; + /** + * Hierarchical authority can provide an hint for the UI about how many levels + * preload to improve the UX. It provides a valid default for hierarchical + * authorities + * + * @return 0 if hierarchical, null otherwise + */ + default Integer getPreloadLevel() { + return isHierarchical() ? 0 : null; } - default public Choice getChoice(String fieldKey, String authKey, String locale) { + /** + * Build the preferred choice associated with the authKey. The default + * implementation delegate the creato to the {@link #getLabel(String, String)} + * {@link #getValue(String, String)} and {@link #getExtra(String, String)} + * methods but can be directly overriden for better efficiency or special + * scenario + * + * @param authKey authority key known to this authority. + * @param locale explicit localization key if available, or null + * @return the preferred choice for this authKey and locale + */ + default public Choice getChoice(String authKey, String locale) { Choice result = new Choice(); result.authority = authKey; - result.label = getLabel(fieldKey, authKey, locale); - result.value = getLabel(fieldKey, authKey, locale); + result.label = getLabel(authKey, locale); + result.value = getValue(authKey, locale); + result.extras.putAll(getExtra(authKey, locale)); return result; } + /** + * Provide a recommendation to store the authority in the metadata value if + * available in the in the provided choice(s). Usually ChoiceAuthority should + * recommend that so the default is true and it only need to be implemented in + * the unusual scenario + * + * @return true if the authority provided in any choice of this + * authority should be stored in the metadata value + */ + default public boolean storeAuthorityInMetadata() { + return true; + } } diff --git a/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthorityServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthorityServiceImpl.java index 4cc3f9d6db..0e05852af0 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthorityServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthorityServiceImpl.java @@ -7,10 +7,13 @@ */ package org.dspace.content.authority; +import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import org.apache.commons.lang3.StringUtils; @@ -19,6 +22,9 @@ import org.dspace.app.util.DCInput; import org.dspace.app.util.DCInputSet; import org.dspace.app.util.DCInputsReader; import org.dspace.app.util.DCInputsReaderException; +import org.dspace.app.util.SubmissionConfig; +import org.dspace.app.util.SubmissionConfigReader; +import org.dspace.app.util.SubmissionConfigReaderException; import org.dspace.content.Collection; import org.dspace.content.MetadataValue; import org.dspace.content.authority.service.ChoiceAuthorityService; @@ -54,23 +60,37 @@ public final class ChoiceAuthorityServiceImpl implements ChoiceAuthorityService // map of field key to authority plugin protected Map controller = new HashMap(); + // map of field key, form definition to authority plugin + protected Map> controllerFormDefinitions = + new HashMap>(); + // map of field key to presentation type protected Map presentation = new HashMap(); // map of field key to closed value protected Map closed = new HashMap(); - // map of authority name to field key - protected Map authorities = new HashMap(); + // flag to track the initialization status of the service + private boolean initialized = false; + + // map of authority name to field keys (the same authority can be configured over multiple metadata) + protected Map> authorities = new HashMap>(); + + // map of authority name to form definition and field keys + protected Map>> authoritiesFormDefinitions = + new HashMap>>(); + + // the item submission reader + private SubmissionConfigReader itemSubmissionConfigReader; @Autowired(required = true) protected ConfigurationService configurationService; @Autowired(required = true) protected PluginService pluginService; - private final String CHOICES_PLUGIN_PREFIX = "choices.plugin."; - private final String CHOICES_PRESENTATION_PREFIX = "choices.presentation."; - private final String CHOICES_CLOSED_PREFIX = "choices.closed."; + final static String CHOICES_PLUGIN_PREFIX = "choices.plugin."; + final static String CHOICES_PRESENTATION_PREFIX = "choices.presentation."; + final static String CHOICES_CLOSED_PREFIX = "choices.closed."; protected ChoiceAuthorityServiceImpl() { } @@ -96,10 +116,25 @@ public final class ChoiceAuthorityServiceImpl implements ChoiceAuthorityService @Override public Set getChoiceAuthoritiesNames() { - if (authorities.keySet().isEmpty()) { + init(); + Set authoritiesNames = new HashSet(); + authoritiesNames.addAll(authorities.keySet()); + authoritiesNames.addAll(authoritiesFormDefinitions.keySet()); + return authoritiesNames; + } + + private synchronized void init() { + if (!initialized) { + try { + itemSubmissionConfigReader = new SubmissionConfigReader(); + } catch (SubmissionConfigReaderException e) { + // the system is in an illegal state as the submission definition is not valid + throw new IllegalStateException("Error reading the item submission configuration: " + e.getMessage(), + e); + } loadChoiceAuthorityConfigurations(); + initialized = true; } - return authorities.keySet(); } @Override @@ -112,59 +147,62 @@ public final class ChoiceAuthorityServiceImpl implements ChoiceAuthorityService @Override public Choices getMatches(String fieldKey, String query, Collection collection, int start, int limit, String locale) { - ChoiceAuthority ma = getChoiceAuthorityMap().get(fieldKey); + ChoiceAuthority ma = getAuthorityByFieldKeyCollection(fieldKey, collection); if (ma == null) { throw new IllegalArgumentException( "No choices plugin was configured for field \"" + fieldKey - + "\"."); + + "\", collection=" + collection.getID().toString() + "."); } - return ma.getMatches(fieldKey, query, collection, start, limit, locale); + return ma.getMatches(query, start, limit, locale); } + @Override public Choices getMatches(String fieldKey, String query, Collection collection, int start, int limit, String locale, boolean externalInput) { - ChoiceAuthority ma = getChoiceAuthorityMap().get(fieldKey); + ChoiceAuthority ma = getAuthorityByFieldKeyCollection(fieldKey, collection); if (ma == null) { throw new IllegalArgumentException( "No choices plugin was configured for field \"" + fieldKey - + "\"."); + + "\", collection=" + collection.getID().toString() + "."); } if (externalInput && ma instanceof SolrAuthority) { ((SolrAuthority) ma).addExternalResultsInNextMatches(); } - return ma.getMatches(fieldKey, query, collection, start, limit, locale); + return ma.getMatches(query, start, limit, locale); } @Override public Choices getBestMatch(String fieldKey, String query, Collection collection, String locale) { - ChoiceAuthority ma = getChoiceAuthorityMap().get(fieldKey); + ChoiceAuthority ma = getAuthorityByFieldKeyCollection(fieldKey, collection); if (ma == null) { throw new IllegalArgumentException( "No choices plugin was configured for field \"" + fieldKey - + "\"."); + + "\", collection=" + collection.getID().toString() + "."); } - return ma.getBestMatch(fieldKey, query, collection, locale); + return ma.getBestMatch(query, locale); } @Override - public String getLabel(MetadataValue metadataValue, String locale) { - return getLabel(metadataValue.getMetadataField().toString(), metadataValue.getAuthority(), locale); + public String getLabel(MetadataValue metadataValue, Collection collection, String locale) { + return getLabel(metadataValue.getMetadataField().toString(), collection, metadataValue.getAuthority(), locale); } @Override - public String getLabel(String fieldKey, String authKey, String locale) { - ChoiceAuthority ma = getChoiceAuthorityMap().get(fieldKey); + public String getLabel(String fieldKey, Collection collection, String authKey, String locale) { + ChoiceAuthority ma = getAuthorityByFieldKeyCollection(fieldKey, collection); if (ma == null) { - throw new IllegalArgumentException("No choices plugin was configured for field \"" + fieldKey + "\"."); + throw new IllegalArgumentException( + "No choices plugin was configured for field \"" + fieldKey + + "\", collection=" + collection.getID().toString() + "."); } - return ma.getLabel(fieldKey, authKey, locale); + return ma.getLabel(authKey, locale); } @Override - public boolean isChoicesConfigured(String fieldKey) { - return getChoiceAuthorityMap().containsKey(fieldKey); + public boolean isChoicesConfigured(String fieldKey, Collection collection) { + return getAuthorityByFieldKeyCollection(fieldKey, collection) != null; } @Override @@ -178,8 +216,14 @@ public final class ChoiceAuthorityServiceImpl implements ChoiceAuthorityService } @Override - public List getVariants(MetadataValue metadataValue) { - ChoiceAuthority ma = getChoiceAuthorityMap().get(metadataValue.getMetadataField().toString()); + public List getVariants(MetadataValue metadataValue, Collection collection) { + String fieldKey = metadataValue.getMetadataField().toString(); + ChoiceAuthority ma = getAuthorityByFieldKeyCollection(fieldKey, collection); + if (ma == null) { + throw new IllegalArgumentException( + "No choices plugin was configured for field \"" + fieldKey + + "\", collection=" + collection.getID().toString() + "."); + } if (ma instanceof AuthorityVariantsSupport) { AuthorityVariantsSupport avs = (AuthorityVariantsSupport) ma; return avs.getVariants(metadataValue.getAuthority(), metadataValue.getLanguage()); @@ -189,42 +233,53 @@ public final class ChoiceAuthorityServiceImpl implements ChoiceAuthorityService @Override - public String getChoiceAuthorityName(String schema, String element, String qualifier) { - String makeFieldKey = makeFieldKey(schema, element, qualifier); - if (getChoiceAuthorityMap().containsKey(makeFieldKey)) { - for (String key : this.authorities.keySet()) { - if (this.authorities.get(key).equals(makeFieldKey)) { - return key; + public String getChoiceAuthorityName(String schema, String element, String qualifier, Collection collection) { + init(); + String fieldKey = makeFieldKey(schema, element, qualifier); + // check if there is an authority configured for the metadata valid for all the collections + if (controller.containsKey(fieldKey)) { + for (Entry> authority2md : authorities.entrySet()) { + if (authority2md.getValue().contains(fieldKey)) { + return authority2md.getKey(); + } + } + } else if (collection != null && controllerFormDefinitions.containsKey(fieldKey)) { + // there is an authority configured for the metadata valid for some collections, + // check if it is the requested collection + Map controllerFormDef = controllerFormDefinitions.get(fieldKey); + SubmissionConfig submissionConfig = itemSubmissionConfigReader + .getSubmissionConfigByCollection(collection.getHandle()); + String submissionName = submissionConfig.getSubmissionName(); + // check if the requested collection has a submission definition that use an authority for the metadata + if (controllerFormDef.containsKey(submissionName)) { + for (Entry>> authority2defs2md : + authoritiesFormDefinitions.entrySet()) { + List mdByDefinition = authority2defs2md.getValue().get(submissionName); + if (mdByDefinition != null && mdByDefinition.contains(fieldKey)) { + return authority2defs2md.getKey(); + } } } } - return configurationService.getProperty( - CHOICES_PLUGIN_PREFIX + schema + "." + element + (qualifier != null ? "." + qualifier : "")); + return null; } protected String makeFieldKey(String schema, String element, String qualifier) { return Utils.standardize(schema, element, qualifier, "_"); } - /** - * Return map of key to ChoiceAuthority plugin - * - * @return - */ - private Map getChoiceAuthorityMap() { - // If empty, load from configuration - if (controller.isEmpty()) { - loadChoiceAuthorityConfigurations(); - } - - return controller; - } - @Override public void clearCache() { controller.clear(); authorities.clear(); + presentation.clear(); + closed.clear(); + controllerFormDefinitions.clear(); + authoritiesFormDefinitions.clear(); + itemSubmissionConfigReader = null; + initialized = false; } + private void loadChoiceAuthorityConfigurations() { // Get all configuration keys starting with a given prefix List propKeys = configurationService.getPropertyKeys(CHOICES_PLUGIN_PREFIX); @@ -249,71 +304,127 @@ public final class ChoiceAuthorityServiceImpl implements ChoiceAuthorityService "Skipping invalid configuration for " + key + " because named plugin not found: " + authorityName); continue; } - if (!authorities.containsKey(authorityName)) { - controller.put(fkey, ma); - authorities.put(authorityName, fkey); - } else { - log.warn( - "Skipping invalid configuration for " + key + " because plugin is alredy in use: " + - authorityName + " used by " + authorities - .get(authorityName)); - continue; - } + controller.put(fkey, ma); + List fkeys; + if (authorities.containsKey(authorityName)) { + fkeys = authorities.get(authorityName); + } else { + fkeys = new ArrayList(); + } + fkeys.add(fkey); + authorities.put(authorityName, fkeys); log.debug("Choice Control: For field=" + fkey + ", Plugin=" + ma); } autoRegisterChoiceAuthorityFromInputReader(); } + /** + * This method will register all the authorities that are required due to the + * submission forms configuration. This includes authorities for value pairs and + * xml vocabularies + */ private void autoRegisterChoiceAuthorityFromInputReader() { try { + List submissionConfigs = itemSubmissionConfigReader + .getAllSubmissionConfigs(Integer.MAX_VALUE, 0); DCInputsReader dcInputsReader = new DCInputsReader(); - for (DCInputSet dcinputSet : dcInputsReader.getAllInputs(Integer.MAX_VALUE, 0)) { - DCInput[][] dcinputs = dcinputSet.getFields(); - for (DCInput[] dcrows : dcinputs) { - for (DCInput dcinput : dcrows) { - if (StringUtils.isNotBlank(dcinput.getPairsType()) - || StringUtils.isNotBlank(dcinput.getVocabulary())) { - String authorityName = dcinput.getPairsType(); - if (StringUtils.isBlank(authorityName)) { + + // loop over all the defined item submission configuration + for (SubmissionConfig subCfg : submissionConfigs) { + String submissionName = subCfg.getSubmissionName(); + List inputsBySubmissionName = dcInputsReader.getInputsBySubmissionName(submissionName); + // loop over the submission forms configuration eventually associated with the submission panel + for (DCInputSet dcinputSet : inputsBySubmissionName) { + DCInput[][] dcinputs = dcinputSet.getFields(); + for (DCInput[] dcrows : dcinputs) { + for (DCInput dcinput : dcrows) { + // for each input in the form check if it is associated with a real value pairs + // or an xml vocabulary + String authorityName = null; + if (StringUtils.isNotBlank(dcinput.getPairsType()) + && !StringUtils.equals(dcinput.getInputType(), "qualdrop_value")) { + authorityName = dcinput.getPairsType(); + } else if (StringUtils.isNotBlank(dcinput.getVocabulary())) { authorityName = dcinput.getVocabulary(); } - if (!StringUtils.equals(dcinput.getInputType(), "qualdrop_value")) { + + // do we have an authority? + if (StringUtils.isNotBlank(authorityName)) { String fieldKey = makeFieldKey(dcinput.getSchema(), dcinput.getElement(), dcinput.getQualifier()); ChoiceAuthority ca = controller.get(authorityName); if (ca == null) { - InputFormSelfRegisterWrapperAuthority ifa = new - InputFormSelfRegisterWrapperAuthority(); - if (controller.containsKey(fieldKey)) { - ifa = (InputFormSelfRegisterWrapperAuthority) controller.get(fieldKey); - } - - ChoiceAuthority ma = (ChoiceAuthority) pluginService + ca = (ChoiceAuthority) pluginService .getNamedPlugin(ChoiceAuthority.class, authorityName); - if (ma == null) { - log.warn("Skipping invalid configuration for " + fieldKey - + " because named plugin not found: " + authorityName); - continue; + if (ca == null) { + throw new IllegalStateException("Invalid configuration for " + fieldKey + + " in submission definition " + submissionName + + ", form definition " + dcinputSet.getFormName() + + " no named plugin found: " + authorityName); } - ifa.getDelegates().put(dcinputSet.getFormName(), ma); - controller.put(fieldKey, ifa); - } - - if (!authorities.containsKey(authorityName)) { - authorities.put(authorityName, fieldKey); } + addAuthorityToFormCacheMap(submissionName, fieldKey, ca); + addFormDetailsToAuthorityCacheMap(submissionName, authorityName, fieldKey); } } } } } } catch (DCInputsReaderException e) { - throw new IllegalStateException(e.getMessage(), e); + // the system is in an illegal state as the submission definition is not valid + throw new IllegalStateException("Error reading the item submission configuration: " + e.getMessage(), + e); } } + /** + * Add the form/field to the cache map keeping track of which form/field are + * associated with the specific authority name + * + * @param submissionName the form definition name + * @param authorityName the name of the authority plugin + * @param fieldKey the field key that use the authority + */ + private void addFormDetailsToAuthorityCacheMap(String submissionName, String authorityName, String fieldKey) { + Map> submissionDefinitionNames2fieldKeys; + if (authoritiesFormDefinitions.containsKey(authorityName)) { + submissionDefinitionNames2fieldKeys = authoritiesFormDefinitions.get(authorityName); + } else { + submissionDefinitionNames2fieldKeys = new HashMap>(); + } + + List fields; + if (submissionDefinitionNames2fieldKeys.containsKey(submissionName)) { + fields = submissionDefinitionNames2fieldKeys.get(submissionName); + } else { + fields = new ArrayList(); + } + fields.add(fieldKey); + submissionDefinitionNames2fieldKeys.put(submissionName, fields); + authoritiesFormDefinitions.put(authorityName, submissionDefinitionNames2fieldKeys); + } + + /** + * Add the authority plugin to the cache map keeping track of which authority is + * used by a specific form/field + * + * @param submissionName the submission definition name + * @param fieldKey the field key that require the authority + * @param ca the authority plugin + */ + private void addAuthorityToFormCacheMap(String submissionName, String fieldKey, ChoiceAuthority ca) { + Map definition2authority; + if (controllerFormDefinitions.containsKey(fieldKey)) { + definition2authority = controllerFormDefinitions.get(fieldKey); + } else { + definition2authority = new HashMap(); + } + definition2authority.put(submissionName, ca); + controllerFormDefinitions.put(fieldKey, definition2authority); + } + /** * Return map of key to presentation * @@ -370,26 +481,6 @@ public final class ChoiceAuthorityServiceImpl implements ChoiceAuthorityService return closed; } - @Override - public String getChoiceMetadatabyAuthorityName(String name) { - if (authorities.isEmpty()) { - loadChoiceAuthorityConfigurations(); - } - if (authorities.containsKey(name)) { - return authorities.get(name); - } - return null; - } - - @Override - public Choice getChoice(String fieldKey, String authKey, String locale) { - ChoiceAuthority ma = getChoiceAuthorityMap().get(fieldKey); - if (ma == null) { - throw new IllegalArgumentException("No choices plugin was configured for field \"" + fieldKey + "\"."); - } - return ma.getChoice(fieldKey, authKey, locale); - } - @Override public ChoiceAuthority getChoiceAuthorityByAuthorityName(String authorityName) { ChoiceAuthority ma = (ChoiceAuthority) @@ -401,4 +492,68 @@ public final class ChoiceAuthorityServiceImpl implements ChoiceAuthorityService } return ma; } + + private ChoiceAuthority getAuthorityByFieldKeyCollection(String fieldKey, Collection collection) { + init(); + ChoiceAuthority ma = controller.get(fieldKey); + if (ma == null && collection != null) { + SubmissionConfigReader configReader; + try { + configReader = new SubmissionConfigReader(); + SubmissionConfig submissionName = configReader.getSubmissionConfigByCollection(collection.getHandle()); + ma = controllerFormDefinitions.get(fieldKey).get(submissionName.getSubmissionName()); + } catch (SubmissionConfigReaderException e) { + // the system is in an illegal state as the submission definition is not valid + throw new IllegalStateException("Error reading the item submission configuration: " + e.getMessage(), + e); + } + } + return ma; + } + + @Override + public boolean storeAuthority(String fieldKey, Collection collection) { + // currently only named authority can eventually provide real authority + return controller.containsKey(fieldKey); + } + + /** + * Wrapper that calls getChoicesByParent method of the plugin. + * + * @param authorityName authority name + * @param parentId parent Id + * @param start choice at which to start, 0 is first. + * @param limit maximum number of choices to return, 0 for no limit. + * @param locale explicit localization key if available, or null + * @return a Choices object (never null). + * @see org.dspace.content.authority.ChoiceAuthority#getChoicesByParent(java.lang.String, java.lang.String, + * int, int, java.lang.String) + */ + @Override + public Choices getChoicesByParent(String authorityName, String parentId, int start, int limit, String locale) { + HierarchicalAuthority ma = (HierarchicalAuthority) getChoiceAuthorityByAuthorityName(authorityName); + return ma.getChoicesByParent(authorityName, parentId, start, limit, locale); + } + + /** + * Wrapper that calls getTopChoices method of the plugin. + * + * @param authorityName authority name + * @param start choice at which to start, 0 is first. + * @param limit maximum number of choices to return, 0 for no limit. + * @param locale explicit localization key if available, or null + * @return a Choices object (never null). + * @see org.dspace.content.authority.ChoiceAuthority#getTopChoices(java.lang.String, int, int, java.lang.String) + */ + @Override + public Choices getTopChoices(String authorityName, int start, int limit, String locale) { + HierarchicalAuthority ma = (HierarchicalAuthority) getChoiceAuthorityByAuthorityName(authorityName); + return ma.getTopChoices(authorityName, start, limit, locale); + } + + @Override + public Choice getParentChoice(String authorityName, String vocabularyId, String locale) { + HierarchicalAuthority ma = (HierarchicalAuthority) getChoiceAuthorityByAuthorityName(authorityName); + return ma.getParentChoice(authorityName, vocabularyId, locale); + } } diff --git a/dspace-api/src/main/java/org/dspace/content/authority/DCInputAuthority.java b/dspace-api/src/main/java/org/dspace/content/authority/DCInputAuthority.java index a64ebdd971..b1d8cf36a5 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/DCInputAuthority.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/DCInputAuthority.java @@ -9,14 +9,20 @@ package org.dspace.content.authority; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.dspace.app.util.DCInputsReader; import org.dspace.app.util.DCInputsReaderException; -import org.dspace.content.Collection; +import org.dspace.core.I18nUtil; import org.dspace.core.SelfNamedPlugin; /** @@ -44,16 +50,38 @@ import org.dspace.core.SelfNamedPlugin; public class DCInputAuthority extends SelfNamedPlugin implements ChoiceAuthority { private static Logger log = org.apache.logging.log4j.LogManager.getLogger(DCInputAuthority.class); - private String values[] = null; - private String labels[] = null; + /** + * The map of the values available for a specific language. Examples of keys are + * "en", "it", "uk" + */ + private Map values = null; - private static DCInputsReader dci = null; + /** + * The map of the labels available for a specific language. Examples of keys are + * "en", "it", "uk" + */ + private Map labels = null; + + /** + * The map of the input form reader associated to use for a specific java locale + */ + private static Map dcis = null; private static String pluginNames[] = null; public DCInputAuthority() { super(); } + @Override + public boolean storeAuthorityInMetadata() { + // For backward compatibility value pairs don't store authority in + // the metadatavalue + return false; + } + public static void reset() { + pluginNames = null; + } + public static String[] getPluginNames() { if (pluginNames == null) { initPluginNames(); @@ -63,20 +91,28 @@ public class DCInputAuthority extends SelfNamedPlugin implements ChoiceAuthority } private static synchronized void initPluginNames() { + Locale[] locales = I18nUtil.getSupportedLocales(); + Set names = new HashSet(); if (pluginNames == null) { try { - if (dci == null) { - dci = new DCInputsReader(); + dcis = new HashMap(); + for (Locale locale : locales) { + dcis.put(locale, new DCInputsReader(I18nUtil.getInputFormsFileName(locale))); + } + for (Locale l : locales) { + Iterator pi = dcis.get(l).getPairsNameIterator(); + while (pi.hasNext()) { + names.add((String) pi.next()); + } + } + DCInputsReader dcirDefault = new DCInputsReader(); + Iterator pi = dcirDefault.getPairsNameIterator(); + while (pi.hasNext()) { + names.add((String) pi.next()); } } catch (DCInputsReaderException e) { log.error("Failed reading DCInputs initialization: ", e); } - List names = new ArrayList(); - Iterator pi = dci.getPairsNameIterator(); - while (pi.hasNext()) { - names.add((String) pi.next()); - } - pluginNames = names.toArray(new String[names.size()]); log.debug("Got plugin names = " + Arrays.deepToString(pluginNames)); } @@ -85,45 +121,65 @@ public class DCInputAuthority extends SelfNamedPlugin implements ChoiceAuthority // once-only load of values and labels private void init() { if (values == null) { + values = new HashMap(); + labels = new HashMap(); String pname = this.getPluginInstanceName(); - List pairs = dci.getPairs(pname); - if (pairs != null) { - values = new String[pairs.size() / 2]; - labels = new String[pairs.size() / 2]; - for (int i = 0; i < pairs.size(); i += 2) { - labels[i / 2] = pairs.get(i); - values[i / 2] = pairs.get(i + 1); + for (Locale l : dcis.keySet()) { + DCInputsReader dci = dcis.get(l); + List pairs = dci.getPairs(pname); + if (pairs != null) { + String[] valuesLocale = new String[pairs.size() / 2]; + String[]labelsLocale = new String[pairs.size() / 2]; + for (int i = 0; i < pairs.size(); i += 2) { + labelsLocale[i / 2] = pairs.get(i); + valuesLocale[i / 2] = pairs.get(i + 1); + } + values.put(l.getLanguage(), valuesLocale); + labels.put(l.getLanguage(), labelsLocale); + log.debug("Found pairs for name=" + pname + ",locale=" + l); + } else { + log.error("Failed to find any pairs for name=" + pname, new IllegalStateException()); } - log.debug("Found pairs for name=" + pname); - } else { - log.error("Failed to find any pairs for name=" + pname, new IllegalStateException()); } + } } @Override - public Choices getMatches(String field, String query, Collection collection, int start, int limit, String locale) { + public Choices getMatches(String query, int start, int limit, String locale) { init(); - + Locale currentLocale = I18nUtil.getSupportedLocale(locale); + String[] valuesLocale = values.get(currentLocale.getLanguage()); + String[] labelsLocale = labels.get(currentLocale.getLanguage()); int dflt = -1; - Choice v[] = new Choice[values.length]; - for (int i = 0; i < values.length; ++i) { - v[i] = new Choice(values[i], values[i], labels[i]); - if (values[i].equalsIgnoreCase(query)) { - dflt = i; + int found = 0; + List v = new ArrayList(); + for (int i = 0; i < valuesLocale.length; ++i) { + if (query == null || StringUtils.containsIgnoreCase(valuesLocale[i], query)) { + if (found >= start && v.size() < limit) { + v.add(new Choice(null, valuesLocale[i], labelsLocale[i])); + if (valuesLocale[i].equalsIgnoreCase(query)) { + dflt = i; + } + } + found++; } } - return new Choices(v, 0, v.length, Choices.CF_AMBIGUOUS, false, dflt); + Choice[] vArray = new Choice[v.size()]; + return new Choices(v.toArray(vArray), start, found, Choices.CF_AMBIGUOUS, false, dflt); } @Override - public Choices getBestMatch(String field, String text, Collection collection, String locale) { + public Choices getBestMatch(String text, String locale) { init(); - for (int i = 0; i < values.length; ++i) { - if (text.equalsIgnoreCase(values[i])) { + Locale currentLocale = I18nUtil.getSupportedLocale(locale); + String[] valuesLocale = values.get(currentLocale.getLanguage()); + String[] labelsLocale = labels.get(currentLocale.getLanguage()); + for (int i = 0; i < valuesLocale.length; ++i) { + if (text.equalsIgnoreCase(valuesLocale[i])) { Choice v[] = new Choice[1]; - v[0] = new Choice(String.valueOf(i), values[i], labels[i]); + v[0] = new Choice(String.valueOf(i), valuesLocale[i], labelsLocale[i]); return new Choices(v, 0, v.length, Choices.CF_UNCERTAIN, false, 0); } } @@ -131,19 +187,31 @@ public class DCInputAuthority extends SelfNamedPlugin implements ChoiceAuthority } @Override - public String getLabel(String field, String key, String locale) { + public String getLabel(String key, String locale) { init(); + + // Get default if locale is empty + if (StringUtils.isBlank(locale)) { + locale = I18nUtil.getDefaultLocale().getLanguage(); + } + + String[] labelsLocale = labels.get(locale); int pos = -1; - for (int i = 0; i < values.length; i++) { - if (values[i].equals(key)) { + for (int i = 0; i < labelsLocale.length; i++) { + if (labelsLocale[i].equals(key)) { pos = i; break; } } if (pos != -1) { - return labels[pos]; + return labelsLocale[pos]; } else { return "UNKNOWN KEY " + key; } } + + @Override + public boolean isScrollable() { + return true; + } } diff --git a/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java b/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java index 097a19eb13..00c74bea9d 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java @@ -10,7 +10,9 @@ package org.dspace.content.authority; import java.io.File; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; @@ -19,7 +21,6 @@ import javax.xml.xpath.XPathFactory; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; -import org.dspace.content.Collection; import org.dspace.core.SelfNamedPlugin; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; @@ -54,25 +55,35 @@ import org.xml.sax.InputSource; * @author Michael B. Klein */ -public class DSpaceControlledVocabulary extends SelfNamedPlugin implements ChoiceAuthority { +public class DSpaceControlledVocabulary extends SelfNamedPlugin implements HierarchicalAuthority { private static Logger log = org.apache.logging.log4j.LogManager.getLogger(DSpaceControlledVocabulary.class); protected static String xpathTemplate = "//node[contains(translate(@label,'ABCDEFGHIJKLMNOPQRSTUVWXYZ'," + "'abcdefghijklmnopqrstuvwxyz'),'%s')]"; protected static String idTemplate = "//node[@id = '%s']"; - protected static String idParentTemplate = "//node[@id = '%s']/parent::isComposedBy"; + protected static String labelTemplate = "//node[@label = '%s']"; + protected static String idParentTemplate = "//node[@id = '%s']/parent::isComposedBy/parent::node"; + protected static String rootTemplate = "/node"; protected static String pluginNames[] = null; protected String vocabularyName = null; protected InputSource vocabulary = null; - protected Boolean suggestHierarchy = true; + protected Boolean suggestHierarchy = false; protected Boolean storeHierarchy = true; protected String hierarchyDelimiter = "::"; + protected Integer preloadLevel = 1; public DSpaceControlledVocabulary() { super(); } + @Override + public boolean storeAuthorityInMetadata() { + // For backward compatibility controlled vocabularies don't store the node id in + // the metadatavalue + return false; + } + public static String[] getPluginNames() { if (pluginNames == null) { initPluginNames(); @@ -112,6 +123,7 @@ public class DSpaceControlledVocabulary extends SelfNamedPlugin implements Choic String configurationPrefix = "vocabulary.plugin." + vocabularyName; storeHierarchy = config.getBooleanProperty(configurationPrefix + ".hierarchy.store", storeHierarchy); suggestHierarchy = config.getBooleanProperty(configurationPrefix + ".hierarchy.suggest", suggestHierarchy); + preloadLevel = config.getIntProperty(configurationPrefix + ".hierarchy.preloadLevel", preloadLevel); String configuredDelimiter = config.getProperty(configurationPrefix + ".delimiter"); if (configuredDelimiter != null) { hierarchyDelimiter = configuredDelimiter.replaceAll("(^\"|\"$)", ""); @@ -142,7 +154,7 @@ public class DSpaceControlledVocabulary extends SelfNamedPlugin implements Choic } @Override - public Choices getMatches(String field, String text, Collection collection, int start, int limit, String locale) { + public Choices getMatches(String text, int start, int limit, String locale) { init(); log.debug("Getting matches for '" + text + "'"); String xpathExpression = ""; @@ -151,59 +163,60 @@ public class DSpaceControlledVocabulary extends SelfNamedPlugin implements Choic xpathExpression += String.format(xpathTemplate, textHierarchy[i].replaceAll("'", "'").toLowerCase()); } XPath xpath = XPathFactory.newInstance().newXPath(); - Choice[] choices; + int total = 0; + List choices = new ArrayList(); try { NodeList results = (NodeList) xpath.evaluate(xpathExpression, vocabulary, XPathConstants.NODESET); - String[] authorities = new String[results.getLength()]; - String[] values = new String[results.getLength()]; - String[] labels = new String[results.getLength()]; - String[] parent = new String[results.getLength()]; - String[] notes = new String[results.getLength()]; - for (int i = 0; i < results.getLength(); i++) { - Node node = results.item(i); - readNode(authorities, values, labels, parent, notes, i, node); - } - int resultCount = labels.length - start; - // limit = 0 means no limit - if ((limit > 0) && (resultCount > limit)) { - resultCount = limit; - } - choices = new Choice[resultCount]; - if (resultCount > 0) { - for (int i = 0; i < resultCount; i++) { - choices[i] = new Choice(authorities[start + i], values[start + i], labels[start + i]); - if (StringUtils.isNotBlank(parent[i])) { - choices[i].extras.put("parent", parent[i]); - } - if (StringUtils.isNotBlank(notes[i])) { - choices[i].extras.put("note", notes[i]); - } - } - } + total = results.getLength(); + choices = getChoicesFromNodeList(results, start, limit); } catch (XPathExpressionException e) { - choices = new Choice[0]; + log.warn(e.getMessage(), e); + return new Choices(true); } - return new Choices(choices, 0, choices.length, Choices.CF_AMBIGUOUS, false); + return new Choices(choices.toArray(new Choice[choices.size()]), start, total, Choices.CF_AMBIGUOUS, + total > start + limit); } @Override - public Choices getBestMatch(String field, String text, Collection collection, String locale) { + public Choices getBestMatch(String text, String locale) { init(); - log.debug("Getting best match for '" + text + "'"); - return getMatches(field, text, collection, 0, 2, locale); - } - - @Override - public String getLabel(String field, String key, String locale) { - init(); - String xpathExpression = String.format(idTemplate, key); + log.debug("Getting best matches for '" + text + "'"); + String xpathExpression = ""; + String[] textHierarchy = text.split(hierarchyDelimiter, -1); + for (int i = 0; i < textHierarchy.length; i++) { + xpathExpression += String.format(labelTemplate, textHierarchy[i].replaceAll("'", "'")); + } XPath xpath = XPathFactory.newInstance().newXPath(); + List choices = new ArrayList(); try { - Node node = (Node) xpath.evaluate(xpathExpression, vocabulary, XPathConstants.NODE); - return node.getAttributes().getNamedItem("label").getNodeValue(); + NodeList results = (NodeList) xpath.evaluate(xpathExpression, vocabulary, XPathConstants.NODESET); + choices = getChoicesFromNodeList(results, 0, 1); } catch (XPathExpressionException e) { - return (""); + log.warn(e.getMessage(), e); + return new Choices(true); } + return new Choices(choices.toArray(new Choice[choices.size()]), 0, choices.size(), Choices.CF_AMBIGUOUS, false); + } + + @Override + public String getLabel(String key, String locale) { + return getNodeLabel(key, this.suggestHierarchy); + } + + @Override + public String getValue(String key, String locale) { + return getNodeLabel(key, this.storeHierarchy); + } + + @Override + public Choice getChoice(String authKey, String locale) { + Node node; + try { + node = getNode(authKey); + } catch (XPathExpressionException e) { + return null; + } + return createChoiceFromNode(node); } @Override @@ -212,81 +225,227 @@ public class DSpaceControlledVocabulary extends SelfNamedPlugin implements Choic } @Override - public Choice getChoice(String fieldKey, String authKey, String locale) { + public Choices getTopChoices(String authorityName, int start, int limit, String locale) { init(); - log.debug("Getting matches for '" + authKey + "'"); - String xpathExpression = String.format(idTemplate, authKey); - XPath xpath = XPathFactory.newInstance().newXPath(); - try { - Node node = (Node) xpath.evaluate(xpathExpression, vocabulary, XPathConstants.NODE); - if (node != null) { - String[] authorities = new String[1]; - String[] values = new String[1]; - String[] labels = new String[1]; - String[] parent = new String[1]; - String[] note = new String[1]; - readNode(authorities, values, labels, parent, note, 0, node); - - if (values.length > 0) { - Choice choice = new Choice(authorities[0], values[0], labels[0]); - if (StringUtils.isNotBlank(parent[0])) { - choice.extras.put("parent", parent[0]); - } - if (StringUtils.isNotBlank(note[0])) { - choice.extras.put("note", note[0]); - } - return choice; - } - } - } catch (XPathExpressionException e) { - log.warn(e.getMessage(), e); - } - return null; + String xpathExpression = rootTemplate; + return getChoicesByXpath(xpathExpression, start, limit); } - private void readNode(String[] authorities, String[] values, String[] labels, String[] parent, String[] notes, - int i, Node node) { + @Override + public Choices getChoicesByParent(String authorityName, String parentId, int start, int limit, String locale) { + init(); + String xpathExpression = String.format(idTemplate, parentId); + return getChoicesByXpath(xpathExpression, start, limit); + } + + @Override + public Choice getParentChoice(String authorityName, String childId, String locale) { + init(); + try { + String xpathExpression = String.format(idParentTemplate, childId); + Choice choice = createChoiceFromNode(getNodeFromXPath(xpathExpression)); + return choice; + } catch (XPathExpressionException e) { + log.error(e.getMessage(), e); + return null; + } + } + + @Override + public Integer getPreloadLevel() { + return preloadLevel; + } + + private boolean isRootElement(Node node) { + if (node != null && node.getOwnerDocument().getDocumentElement().equals(node)) { + return true; + } + return false; + } + + private Node getNode(String key) throws XPathExpressionException { + init(); + String xpathExpression = String.format(idTemplate, key); + Node node = getNodeFromXPath(xpathExpression); + return node; + } + + private Node getNodeFromXPath(String xpathExpression) throws XPathExpressionException { + XPath xpath = XPathFactory.newInstance().newXPath(); + Node node = (Node) xpath.evaluate(xpathExpression, vocabulary, XPathConstants.NODE); + return node; + } + + private List getChoicesFromNodeList(NodeList results, int start, int limit) { + List choices = new ArrayList(); + for (int i = 0; i < results.getLength(); i++) { + if (i < start) { + continue; + } + if (choices.size() == limit) { + break; + } + Node node = results.item(i); + Choice choice = new Choice(getAuthority(node), getLabel(node), getValue(node), + isSelectable(node)); + choice.extras = addOtherInformation(getParent(node), getNote(node), getChildren(node), getAuthority(node)); + choices.add(choice); + } + return choices; + } + + private Map addOtherInformation(String parentCurr, String noteCurr, + List childrenCurr, String authorityCurr) { + Map extras = new HashMap(); + if (StringUtils.isNotBlank(parentCurr)) { + extras.put("parent", parentCurr); + } + if (StringUtils.isNotBlank(noteCurr)) { + extras.put("note", noteCurr); + } + if (childrenCurr.size() > 0) { + extras.put("hasChildren", "true"); + } else { + extras.put("hasChildren", "false"); + } + extras.put("id", authorityCurr); + return extras; + } + + private String getNodeLabel(String key, boolean useHierarchy) { + try { + Node node = getNode(key); + if (useHierarchy) { + return this.buildString(node); + } else { + return node.getAttributes().getNamedItem("label").getNodeValue(); + } + } catch (XPathExpressionException e) { + return (""); + } + } + + private String getLabel(Node node) { String hierarchy = this.buildString(node); if (this.suggestHierarchy) { - labels[i] = hierarchy; + return hierarchy; } else { - labels[i] = node.getAttributes().getNamedItem("label").getNodeValue(); - } - if (this.storeHierarchy) { - values[i] = hierarchy; - } else { - values[i] = node.getAttributes().getNamedItem("label").getNodeValue(); + return node.getAttributes().getNamedItem("label").getNodeValue(); } + } + private String getValue(Node node) { + String hierarchy = this.buildString(node); + if (this.storeHierarchy) { + return hierarchy; + } else { + return node.getAttributes().getNamedItem("label").getNodeValue(); + } + } + + private String getNote(Node node) { NodeList childNodes = node.getChildNodes(); for (int ci = 0; ci < childNodes.getLength(); ci++) { Node firstChild = childNodes.item(ci); if (firstChild != null && "hasNote".equals(firstChild.getNodeName())) { String nodeValue = firstChild.getTextContent(); if (StringUtils.isNotBlank(nodeValue)) { - notes[i] = nodeValue; + return nodeValue; } } } - Node idAttr = node.getAttributes().getNamedItem("id"); - if (null != idAttr) { // 'id' is optional - authorities[i] = idAttr.getNodeValue(); - if (isHierarchical()) { - Node parentN = node.getParentNode(); - if (parentN != null) { - parentN = parentN.getParentNode(); - if (parentN != null) { - Node parentIdAttr = parentN.getAttributes().getNamedItem("id"); - if (null != parentIdAttr) { - parent[i] = parentIdAttr.getNodeValue(); + return null; + } + + private List getChildren(Node node) { + List children = new ArrayList(); + NodeList childNodes = node.getChildNodes(); + for (int ci = 0; ci < childNodes.getLength(); ci++) { + Node firstChild = childNodes.item(ci); + if (firstChild != null && "isComposedBy".equals(firstChild.getNodeName())) { + for (int cii = 0; cii < firstChild.getChildNodes().getLength(); cii++) { + Node childN = firstChild.getChildNodes().item(cii); + if (childN != null && "node".equals(childN.getNodeName())) { + Node childIdAttr = childN.getAttributes().getNamedItem("id"); + if (null != childIdAttr) { + children.add(childIdAttr.getNodeValue()); } } } + break; } - } else { - authorities[i] = null; - parent[i] = null; + } + return children; + } + + private boolean isSelectable(Node node) { + Node selectableAttr = node.getAttributes().getNamedItem("selectable"); + if (null != selectableAttr) { + return Boolean.valueOf(selectableAttr.getNodeValue()); + } else { // Default is true + return true; } } + private String getParent(Node node) { + Node parentN = node.getParentNode(); + if (parentN != null) { + parentN = parentN.getParentNode(); + if (parentN != null && !isRootElement(parentN)) { + return buildString(parentN); + } + } + return null; + } + + private String getAuthority(Node node) { + Node idAttr = node.getAttributes().getNamedItem("id"); + if (null != idAttr) { // 'id' is optional + return idAttr.getNodeValue(); + } else { + return null; + } + } + + private Choices getChoicesByXpath(String xpathExpression, int start, int limit) { + List choices = new ArrayList(); + XPath xpath = XPathFactory.newInstance().newXPath(); + try { + Node parentNode = (Node) xpath.evaluate(xpathExpression, vocabulary, XPathConstants.NODE); + int count = 0; + if (parentNode != null) { + NodeList childNodes = (NodeList) xpath.evaluate(".//isComposedBy", parentNode, XPathConstants.NODE); + if (null != childNodes) { + for (int i = 0; i < childNodes.getLength(); i++) { + Node childNode = childNodes.item(i); + if (childNode != null && "node".equals(childNode.getNodeName())) { + if (count < start || choices.size() >= limit) { + count++; + continue; + } + count++; + choices.add(createChoiceFromNode(childNode)); + } + } + } + return new Choices(choices.toArray(new Choice[choices.size()]), start, count, + Choices.CF_AMBIGUOUS, false); + } + } catch (XPathExpressionException e) { + log.warn(e.getMessage(), e); + return new Choices(true); + } + return new Choices(false); + } + + private Choice createChoiceFromNode(Node node) { + if (node != null && !isRootElement(node)) { + Choice choice = new Choice(getAuthority(node), getLabel(node), getValue(node), + isSelectable(node)); + choice.extras = addOtherInformation(getParent(node), getNote(node),getChildren(node), getAuthority(node)); + return choice; + } + return null; + } + } diff --git a/dspace-api/src/main/java/org/dspace/content/authority/HierarchicalAuthority.java b/dspace-api/src/main/java/org/dspace/content/authority/HierarchicalAuthority.java new file mode 100644 index 0000000000..c25b74d354 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/authority/HierarchicalAuthority.java @@ -0,0 +1,85 @@ +/** + * 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.authority; + +/** + * Plugin interface that supplies an authority control mechanism for + * one metadata field. + * + * @author Larry Stone + * @see ChoiceAuthority + */ +public interface HierarchicalAuthority extends ChoiceAuthority { + + /** + * Get all values from the authority that match the preferred value. + * Note that the offering was entered by the user and may contain + * mixed/incorrect case, whitespace, etc so the plugin should be careful + * to clean up user data before making comparisons. + * + * Value of a "Name" field will be in canonical DSpace person name format, + * which is "Lastname, Firstname(s)", e.g. "Smith, John Q.". + * + * Some authorities with a small set of values may simply return the whole + * set for any sample value, although it's a good idea to set the + * defaultSelected index in the Choices instance to the choice, if any, + * that matches the value. + * + * @param authorityName authority name + * @param start choice at which to start, 0 is first. + * @param limit maximum number of choices to return, 0 for no limit. + * @param locale explicit localization key if available, or null + * @return a Choices object (never null). + */ + public Choices getTopChoices(String authorityName, int start, int limit, String locale); + + /** + * Get all values from the authority that match the preferred value. + * Note that the offering was entered by the user and may contain + * mixed/incorrect case, whitespace, etc so the plugin should be careful + * to clean up user data before making comparisons. + * + * Value of a "Name" field will be in canonical DSpace person name format, + * which is "Lastname, Firstname(s)", e.g. "Smith, John Q.". + * + * Some authorities with a small set of values may simply return the whole + * set for any sample value, although it's a good idea to set the + * defaultSelected index in the Choices instance to the choice, if any, + * that matches the value. + * + * @param authorityName authority name + * @param parentId user's value to match + * @param start choice at which to start, 0 is first. + * @param limit maximum number of choices to return, 0 for no limit. + * @param locale explicit localization key if available, or null + * @return a Choices object (never null). + */ + public Choices getChoicesByParent(String authorityName, String parentId, int start, int limit, String locale); + + /** + * It returns the parent choice in the hierarchy if any + * + * @param authorityName authority name + * @param vocabularyId user's value to match + * @param locale explicit localization key if available, or null + * @return a Choice object + */ + public Choice getParentChoice(String authorityName, String vocabularyId, String locale); + + /** + * Provides an hint for the UI to preload some levels to improve the UX. It + * usually mean that these preloaded level will be shown expanded by default + */ + public Integer getPreloadLevel(); + + @Override + default boolean isHierarchical() { + return true; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/content/authority/InputFormSelfRegisterWrapperAuthority.java b/dspace-api/src/main/java/org/dspace/content/authority/InputFormSelfRegisterWrapperAuthority.java deleted file mode 100644 index 8716ef38b9..0000000000 --- a/dspace-api/src/main/java/org/dspace/content/authority/InputFormSelfRegisterWrapperAuthority.java +++ /dev/null @@ -1,166 +0,0 @@ -/** - * 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.authority; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.Logger; -import org.dspace.app.util.DCInputsReader; -import org.dspace.app.util.DCInputsReaderException; -import org.dspace.content.Collection; - -/** - * This authority is registered automatically by the ChoiceAuthorityService for - * all the metadata that use a value-pair or a vocabulary in the submission-form.xml - * - * It keeps a map of form-name vs ChoiceAuthority to delegate the execution of - * the method to the specific ChoiceAuthority configured for the collection when - * the same metadata have different vocabulary or value-pair on a collection - * basis - * - * @author Andrea Bollini (andrea.bollini at 4science.it) - */ -public class InputFormSelfRegisterWrapperAuthority implements ChoiceAuthority { - - private static Logger log = - org.apache.logging.log4j.LogManager.getLogger(InputFormSelfRegisterWrapperAuthority.class); - - private Map delegates = new HashMap(); - - private static DCInputsReader dci = null; - - private void init() { - try { - if (dci == null) { - dci = new DCInputsReader(); - } - } catch (DCInputsReaderException e) { - log.error("Failed reading DCInputs initialization: ", e); - } - } - - @Override - public Choices getMatches(String field, String query, Collection collection, int start, int limit, String locale) { - String formName; - try { - init(); - if (collection == null) { - Set choices = new HashSet(); - //workaround search in all authority configured - for (ChoiceAuthority ca : delegates.values()) { - Choices tmp = ca.getMatches(field, query, null, start, limit, locale); - if (tmp.total > 0) { - Set mySet = new HashSet(Arrays.asList(tmp.values)); - choices.addAll(mySet); - } - } - if (!choices.isEmpty()) { - Choice[] results = new Choice[choices.size()]; - choices.toArray(results); - return new Choices(results, 0, choices.size(), Choices.CF_AMBIGUOUS, false); - } - } else { - formName = dci.getInputFormNameByCollectionAndField(collection, field); - return delegates.get(formName).getMatches(field, query, collection, start, limit, locale); - } - } catch (DCInputsReaderException e) { - log.error(e.getMessage(), e); - } - return new Choices(Choices.CF_NOTFOUND); - } - - @Override - public Choices getBestMatch(String field, String text, Collection collection, String locale) { - String formName; - try { - init(); - if (collection == null) { - Set choices = new HashSet(); - //workaround search in all authority configured - for (ChoiceAuthority ca : delegates.values()) { - Choices tmp = ca.getBestMatch(field, text, null, locale); - if (tmp.total > 0) { - Set mySet = new HashSet(Arrays.asList(tmp.values)); - choices.addAll(mySet); - } - } - if (!choices.isEmpty()) { - Choice[] results = new Choice[choices.size() - 1]; - choices.toArray(results); - return new Choices(results, 0, choices.size(), Choices.CF_UNCERTAIN, false); - } - } else { - formName = dci.getInputFormNameByCollectionAndField(collection, field); - return delegates.get(formName).getBestMatch(field, text, collection, locale); - } - } catch (DCInputsReaderException e) { - log.error(e.getMessage(), e); - } - return new Choices(Choices.CF_NOTFOUND); - } - - @Override - public String getLabel(String field, String key, String locale) { - // TODO we need to manage REALLY the authority - // WRONG BEHAVIOUR: now in each delegates can exists the same key with - // different value - for (ChoiceAuthority delegate : delegates.values()) { - String label = delegate.getLabel(field, key, locale); - if (StringUtils.isNotBlank(label)) { - return label; - } - } - return "UNKNOWN KEY " + key; - } - - @Override - public boolean isHierarchical() { - // TODO we need to manage REALLY the authority - // WRONG BEHAVIOUR: now in each delegates can exists the same key with - // different value - for (ChoiceAuthority delegate : delegates.values()) { - return delegate.isHierarchical(); - } - return false; - } - - @Override - public boolean isScrollable() { - // TODO we need to manage REALLY the authority - // WRONG BEHAVIOUR: now in each delegates can exists the same key with - // different value - for (ChoiceAuthority delegate : delegates.values()) { - return delegate.isScrollable(); - } - return false; - } - - @Override - public boolean hasIdentifier() { - // TODO we need to manage REALLY the authority - // WRONG BEHAVIOUR: now in each delegates can exists the same key with - // different value - for (ChoiceAuthority delegate : delegates.values()) { - return delegate.hasIdentifier(); - } - return false; - } - - public Map getDelegates() { - return delegates; - } - - public void setDelegates(Map delegates) { - this.delegates = delegates; - } -} diff --git a/dspace-api/src/main/java/org/dspace/content/authority/MetadataAuthorityServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/authority/MetadataAuthorityServiceImpl.java index 6a5b17a029..c542c6a89e 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/MetadataAuthorityServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/MetadataAuthorityServiceImpl.java @@ -14,12 +14,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; -import org.dspace.app.util.DCInput; -import org.dspace.app.util.DCInputSet; -import org.dspace.app.util.DCInputsReader; -import org.dspace.app.util.DCInputsReaderException; import org.dspace.content.MetadataField; import org.dspace.content.authority.service.MetadataAuthorityService; import org.dspace.content.service.MetadataFieldService; @@ -144,8 +139,6 @@ public class MetadataAuthorityServiceImpl implements MetadataAuthorityService { if (dmc >= Choices.CF_UNSET) { defaultMinConfidence = dmc; } - - autoRegisterAuthorityFromInputReader(); } } @@ -205,7 +198,6 @@ public class MetadataAuthorityServiceImpl implements MetadataAuthorityService { } } - /** * Give the minimal level of confidence required to consider valid an authority value * for the given metadata. @@ -229,35 +221,4 @@ public class MetadataAuthorityServiceImpl implements MetadataAuthorityService { } return copy; } - - - private void autoRegisterAuthorityFromInputReader() { - try { - DCInputsReader dcInputsReader = new DCInputsReader(); - for (DCInputSet dcinputSet : dcInputsReader.getAllInputs(Integer.MAX_VALUE, 0)) { - DCInput[][] dcinputs = dcinputSet.getFields(); - for (DCInput[] dcrows : dcinputs) { - for (DCInput dcinput : dcrows) { - if (StringUtils.isNotBlank(dcinput.getPairsType()) - || StringUtils.isNotBlank(dcinput.getVocabulary())) { - String authorityName = dcinput.getPairsType(); - if (StringUtils.isBlank(authorityName)) { - authorityName = dcinput.getVocabulary(); - } - if (!StringUtils.equals(dcinput.getInputType(), "qualdrop_value")) { - String fieldKey = makeFieldKey(dcinput.getSchema(), dcinput.getElement(), - dcinput.getQualifier()); - boolean req = ConfigurationManager - .getBooleanProperty("authority.required." + fieldKey, false); - controlled.put(fieldKey, true); - isAuthorityRequired.put(fieldKey, req); - } - } - } - } - } - } catch (DCInputsReaderException e) { - throw new IllegalStateException(e.getMessage(), e); - } - } } diff --git a/dspace-api/src/main/java/org/dspace/content/authority/SampleAuthority.java b/dspace-api/src/main/java/org/dspace/content/authority/SampleAuthority.java index 8197f180af..e6cc9b9d44 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/SampleAuthority.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/SampleAuthority.java @@ -7,13 +7,13 @@ */ package org.dspace.content.authority; -import org.dspace.content.Collection; - /** * This is a *very* stupid test fixture for authority control, and also * serves as a trivial example of an authority plugin implementation. */ public class SampleAuthority implements ChoiceAuthority { + private String pluginInstanceName; + protected static String values[] = { "sun", "mon", @@ -35,7 +35,7 @@ public class SampleAuthority implements ChoiceAuthority { }; @Override - public Choices getMatches(String field, String query, Collection collection, int start, int limit, String locale) { + public Choices getMatches(String query, int start, int limit, String locale) { int dflt = -1; Choice v[] = new Choice[values.length]; for (int i = 0; i < values.length; ++i) { @@ -48,7 +48,7 @@ public class SampleAuthority implements ChoiceAuthority { } @Override - public Choices getBestMatch(String field, String text, Collection collection, String locale) { + public Choices getBestMatch(String text, String locale) { for (int i = 0; i < values.length; ++i) { if (text.equalsIgnoreCase(values[i])) { Choice v[] = new Choice[1]; @@ -60,7 +60,17 @@ public class SampleAuthority implements ChoiceAuthority { } @Override - public String getLabel(String field, String key, String locale) { + public String getLabel(String key, String locale) { return labels[Integer.parseInt(key)]; } + + @Override + public String getPluginInstanceName() { + return pluginInstanceName; + } + + @Override + public void setPluginInstanceName(String name) { + this.pluginInstanceName = name; + } } diff --git a/dspace-api/src/main/java/org/dspace/content/authority/SolrAuthority.java b/dspace-api/src/main/java/org/dspace/content/authority/SolrAuthority.java index 5e913430b7..c93e6db786 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/SolrAuthority.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/SolrAuthority.java @@ -11,6 +11,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; @@ -24,8 +25,9 @@ import org.dspace.authority.AuthorityValue; import org.dspace.authority.SolrAuthorityInterface; import org.dspace.authority.factory.AuthorityServiceFactory; import org.dspace.authority.service.AuthorityValueService; -import org.dspace.content.Collection; import org.dspace.core.ConfigurationManager; +import org.dspace.core.NameAwarePlugin; +import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; /** @@ -35,7 +37,14 @@ import org.dspace.services.factory.DSpaceServicesFactory; * @author Mark Diggory (markd at atmire dot com) */ public class SolrAuthority implements ChoiceAuthority { + /** the name assigned to the specific instance by the PluginService, @see {@link NameAwarePlugin} **/ + private String authorityName; + /** + * the metadata managed by the plugin instance, derived from its authority name + * in the form schema_element_qualifier + */ + private String field; protected SolrAuthorityInterface source = DSpaceServicesFactory.getInstance().getServiceManager() .getServiceByName("AuthoritySource", SolrAuthorityInterface.class); @@ -45,8 +54,9 @@ public class SolrAuthority implements ChoiceAuthority { protected boolean externalResults = false; protected final AuthorityValueService authorityValueService = AuthorityServiceFactory.getInstance() .getAuthorityValueService(); - - public Choices getMatches(String field, String text, Collection collection, int start, int limit, String locale, + protected final ConfigurationService configurationService = DSpaceServicesFactory.getInstance() + .getConfigurationService(); + public Choices getMatches(String text, int start, int limit, String locale, boolean bestMatch) { if (limit == 0) { limit = 10; @@ -193,13 +203,13 @@ public class SolrAuthority implements ChoiceAuthority { } @Override - public Choices getMatches(String field, String text, Collection collection, int start, int limit, String locale) { - return getMatches(field, text, collection, start, limit, locale, true); + public Choices getMatches(String text, int start, int limit, String locale) { + return getMatches(text, start, limit, locale, true); } @Override - public Choices getBestMatch(String field, String text, Collection collection, String locale) { - Choices matches = getMatches(field, text, collection, 0, 1, locale, false); + public Choices getBestMatch(String text, String locale) { + Choices matches = getMatches(text, 0, 1, locale, false); if (matches.values.length != 0 && !matches.values[0].value.equalsIgnoreCase(text)) { matches = new Choices(false); } @@ -207,7 +217,7 @@ public class SolrAuthority implements ChoiceAuthority { } @Override - public String getLabel(String field, String key, String locale) { + public String getLabel(String key, String locale) { try { if (log.isDebugEnabled()) { log.debug("requesting label for key " + key + " using locale " + locale); @@ -276,4 +286,23 @@ public class SolrAuthority implements ChoiceAuthority { public void addExternalResultsInNextMatches() { this.externalResults = true; } + + @Override + public void setPluginInstanceName(String name) { + authorityName = name; + for (Entry conf : configurationService.getProperties().entrySet()) { + if (StringUtils.startsWith((String) conf.getKey(), ChoiceAuthorityServiceImpl.CHOICES_PLUGIN_PREFIX) + && StringUtils.equals((String) conf.getValue(), authorityName)) { + field = ((String) conf.getKey()).substring(ChoiceAuthorityServiceImpl.CHOICES_PLUGIN_PREFIX.length()) + .replace(".", "_"); + // exit the look immediately as we have found it + break; + } + } + } + + @Override + public String getPluginInstanceName() { + return authorityName; + } } diff --git a/dspace-api/src/main/java/org/dspace/content/authority/TestAuthority.java b/dspace-api/src/main/java/org/dspace/content/authority/TestAuthority.java index a017e8fe28..15c000e978 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/TestAuthority.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/TestAuthority.java @@ -11,7 +11,6 @@ import java.util.ArrayList; import java.util.List; import org.apache.commons.lang3.StringUtils; -import org.dspace.content.Collection; /** * This is a *very* stupid test fixture for authority control with AuthorityVariantsSupport. @@ -19,6 +18,7 @@ import org.dspace.content.Collection; * @author Andrea Bollini (CILEA) */ public class TestAuthority implements ChoiceAuthority, AuthorityVariantsSupport { + private String pluginInstanceName; @Override public List getVariants(String key, String locale) { @@ -33,8 +33,7 @@ public class TestAuthority implements ChoiceAuthority, AuthorityVariantsSupport } @Override - public Choices getMatches(String field, String text, Collection collection, - int start, int limit, String locale) { + public Choices getMatches(String text, int start, int limit, String locale) { Choices choices = new Choices(false); if (StringUtils.isNotBlank(text)) { @@ -52,8 +51,7 @@ public class TestAuthority implements ChoiceAuthority, AuthorityVariantsSupport } @Override - public Choices getBestMatch(String field, String text, Collection collection, - String locale) { + public Choices getBestMatch(String text, String locale) { Choices choices = new Choices(false); if (StringUtils.isNotBlank(text)) { @@ -70,10 +68,20 @@ public class TestAuthority implements ChoiceAuthority, AuthorityVariantsSupport } @Override - public String getLabel(String field, String key, String locale) { + public String getLabel(String key, String locale) { if (StringUtils.isNotBlank(key)) { return key.replaceAll("authority", "label"); } return "Unknown"; } + + @Override + public String getPluginInstanceName() { + return pluginInstanceName; + } + + @Override + public void setPluginInstanceName(String name) { + this.pluginInstanceName = name; + } } diff --git a/dspace-api/src/main/java/org/dspace/content/authority/service/ChoiceAuthorityService.java b/dspace-api/src/main/java/org/dspace/content/authority/service/ChoiceAuthorityService.java index 83db9a734e..1cc5075d02 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/service/ChoiceAuthorityService.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/service/ChoiceAuthorityService.java @@ -48,10 +48,10 @@ public interface ChoiceAuthorityService { * @param element element of metadata field * @param qualifier qualifier of metadata field * @return the name of the choice authority associated with the specified - * metadata. Throw IllegalArgumentException if the supplied metadat + * metadata. Throw IllegalArgumentException if the supplied metadata * is not associated with an authority choice */ - public String getChoiceAuthorityName(String schema, String element, String qualifier); + public String getChoiceAuthorityName(String schema, String element, String qualifier, Collection collection); /** * Wrapper that calls getMatches method of the plugin corresponding to @@ -112,30 +112,33 @@ public interface ChoiceAuthorityService { * the metadata field defined by schema,element,qualifier. * * @param metadataValue metadata value + * @param collection Collection owner of Item * @param locale explicit localization key if available * @return label */ - public String getLabel(MetadataValue metadataValue, String locale); + public String getLabel(MetadataValue metadataValue, Collection collection, String locale); /** * Wrapper that calls getLabel method of the plugin corresponding to * the metadata field defined by single field key. * * @param fieldKey single string identifying metadata field + * @param collection Collection owner of Item * @param locale explicit localization key if available * @param authKey authority key * @return label */ - public String getLabel(String fieldKey, String authKey, String locale); + public String getLabel(String fieldKey, Collection collection, String authKey, String locale); /** * Predicate, is there a Choices configuration of any kind for the * given metadata field? * * @param fieldKey single string identifying metadata field + * @param collection Collection owner of Item * @return true if choices are configured for this field. */ - public boolean isChoicesConfigured(String fieldKey); + public boolean isChoicesConfigured(String fieldKey, Collection collection); /** * Get the presentation keyword (should be "lookup", "select" or "suggest", but this @@ -160,12 +163,14 @@ public interface ChoiceAuthorityService { * @param metadataValue metadata value * @return List of variants */ - public List getVariants(MetadataValue metadataValue); - - public String getChoiceMetadatabyAuthorityName(String name); - - public Choice getChoice(String fieldKey, String authKey, String locale); + public List getVariants(MetadataValue metadataValue, Collection collection); + /** + * Return the ChoiceAuthority instance identified by the specified name + * + * @param authorityName the ChoiceAuthority instance name + * @return the ChoiceAuthority identified by the specified name + */ public ChoiceAuthority getChoiceAuthorityByAuthorityName(String authorityName); /** @@ -173,4 +178,49 @@ public interface ChoiceAuthorityService { */ public void clearCache(); + /** + * Should we store the authority key (if any) for such field key and collection? + * + * @param fieldKey single string identifying metadata field + * @param collection Collection owner of Item or where the item is submitted to + * @return true if the configuration allows to store the authority value + */ + public boolean storeAuthority(String fieldKey, Collection collection); + + /** + * Wrapper that calls getChoicesByParent method of the plugin. + * + * @param authorityName authority name + * @param parentId parent Id + * @param start choice at which to start, 0 is first. + * @param limit maximum number of choices to return, 0 for no limit. + * @param locale explicit localization key if available, or null + * @return a Choices object (never null). + * @see org.dspace.content.authority.ChoiceAuthority#getChoicesByParent(java.lang.String, java.lang.String, + * int, int, java.lang.String) + */ + public Choices getChoicesByParent(String authorityName, String parentId, int start, int limit, String locale); + + /** + * Wrapper that calls getTopChoices method of the plugin. + * + * @param authorityName authority name + * @param start choice at which to start, 0 is first. + * @param limit maximum number of choices to return, 0 for no limit. + * @param locale explicit localization key if available, or null + * @return a Choices object (never null). + * @see org.dspace.content.authority.ChoiceAuthority#getTopChoices(java.lang.String, int, int, java.lang.String) + */ + public Choices getTopChoices(String authorityName, int start, int limit, String locale); + + /** + * Return the direct parent of an entry identified by its id in an hierarchical + * authority. + * + * @param authorityName authority name + * @param vocabularyId child id + * @param locale explicit localization key if available, or null + * @return the parent Choice object if any + */ + public Choice getParentChoice(String authorityName, String vocabularyId, String locale); } diff --git a/dspace-api/src/main/java/org/dspace/core/I18nUtil.java b/dspace-api/src/main/java/org/dspace/core/I18nUtil.java index d06b1e9bc8..cd0609e29f 100644 --- a/dspace-api/src/main/java/org/dspace/core/I18nUtil.java +++ b/dspace-api/src/main/java/org/dspace/core/I18nUtil.java @@ -191,6 +191,23 @@ public class I18nUtil { return supportedLocale; } + /** + * Gets the appropriate supported Locale according for a given Locale If + * no appropriate supported locale is found, the DEFAULTLOCALE is used + * + * @param locale String to find the corresponding Locale + * @return supportedLocale + * Locale for session according to locales supported by this DSpace instance as set in dspace.cfg + */ + public static Locale getSupportedLocale(String locale) { + Locale currentLocale = null; + if (locale != null) { + currentLocale = I18nUtil.getSupportedLocale(new Locale(locale)); + } else { + currentLocale = I18nUtil.getDefaultLocale(); + } + return currentLocale; + } /** * Get the appropriate localized version of submission-forms.xml according to language settings diff --git a/dspace-api/src/main/java/org/dspace/core/LegacyPluginServiceImpl.java b/dspace-api/src/main/java/org/dspace/core/LegacyPluginServiceImpl.java index f8291dc977..ea8cdc1403 100644 --- a/dspace-api/src/main/java/org/dspace/core/LegacyPluginServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/core/LegacyPluginServiceImpl.java @@ -345,8 +345,8 @@ public class LegacyPluginServiceImpl implements PluginService { " for interface=" + iname + " pluginName=" + name); Object result = pluginClass.newInstance(); - if (result instanceof SelfNamedPlugin) { - ((SelfNamedPlugin) result).setPluginInstanceName(name); + if (result instanceof NameAwarePlugin) { + ((NameAwarePlugin) result).setPluginInstanceName(name); } return result; } diff --git a/dspace-api/src/main/java/org/dspace/core/NameAwarePlugin.java b/dspace-api/src/main/java/org/dspace/core/NameAwarePlugin.java new file mode 100644 index 0000000000..6c562ea04c --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/core/NameAwarePlugin.java @@ -0,0 +1,42 @@ +/** + * 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.core; + +/** + * This is the interface that should be implemented by all the named plugin that + * like to be aware of their name + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + * @version $Revision$ + * @see org.dspace.core.service.PluginService + */ +public interface NameAwarePlugin { + + /** + * Get the instance's particular name. + * Returns the name by which the class was chosen when + * this instance was created. Only works for instances created + * by PluginService, or if someone remembers to call setPluginName. + *

+ * Useful when the implementation class wants to be configured differently + * when it is invoked under different names. + * + * @return name or null if not available. + */ + public String getPluginInstanceName(); + + /** + * Set the name under which this plugin was instantiated. + * Not to be invoked by application code, it is + * called automatically by PluginService.getNamedPlugin() + * when the plugin is instantiated. + * + * @param name -- name used to select this class. + */ + public void setPluginInstanceName(String name); +} diff --git a/dspace-api/src/main/java/org/dspace/core/SelfNamedPlugin.java b/dspace-api/src/main/java/org/dspace/core/SelfNamedPlugin.java index 2bdcf830e7..680fa15c80 100644 --- a/dspace-api/src/main/java/org/dspace/core/SelfNamedPlugin.java +++ b/dspace-api/src/main/java/org/dspace/core/SelfNamedPlugin.java @@ -28,7 +28,7 @@ package org.dspace.core; * @version $Revision$ * @see org.dspace.core.service.PluginService */ -public abstract class SelfNamedPlugin { +public abstract class SelfNamedPlugin implements NameAwarePlugin { // the specific alias used to find the class that created this instance. private String myName = null; @@ -52,30 +52,13 @@ public abstract class SelfNamedPlugin { return null; } - /** - * Get an instance's particular name. - * Returns the name by which the class was chosen when - * this instance was created. Only works for instances created - * by PluginService, or if someone remembers to call setPluginName. - *

- * Useful when the implementation class wants to be configured differently - * when it is invoked under different names. - * - * @return name or null if not available. - */ + @Override public String getPluginInstanceName() { return myName; } - /** - * Set the name under which this plugin was instantiated. - * Not to be invoked by application code, it is - * called automatically by PluginService.getNamedPlugin() - * when the plugin is instantiated. - * - * @param name -- name used to select this class. - */ - protected void setPluginInstanceName(String name) { + @Override + public void setPluginInstanceName(String name) { myName = name; } } diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceMetadataBrowseIndexingPlugin.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceMetadataBrowseIndexingPlugin.java index 187c6b0600..2b2be66384 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceMetadataBrowseIndexingPlugin.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceMetadataBrowseIndexingPlugin.java @@ -17,6 +17,7 @@ import org.apache.logging.log4j.Logger; import org.apache.solr.common.SolrInputDocument; import org.dspace.browse.BrowseException; import org.dspace.browse.BrowseIndex; +import org.dspace.content.Collection; import org.dspace.content.Item; import org.dspace.content.MetadataValue; import org.dspace.content.authority.service.ChoiceAuthorityService; @@ -63,7 +64,7 @@ public class SolrServiceMetadataBrowseIndexingPlugin implements SolrServiceIndex return; } Item item = ((IndexableItem) indexableObject).getIndexedObject(); - + Collection collection = item.getOwningCollection(); // Get the currently configured browse indexes BrowseIndex[] bis; try { @@ -175,7 +176,7 @@ public class SolrServiceMetadataBrowseIndexingPlugin implements SolrServiceIndex true); if (!ignorePrefered) { preferedLabel = choiceAuthorityService - .getLabel(values.get(x), values.get(x).getLanguage()); + .getLabel(values.get(x), collection, values.get(x).getLanguage()); } List variants = null; @@ -195,7 +196,7 @@ public class SolrServiceMetadataBrowseIndexingPlugin implements SolrServiceIndex if (!ignoreVariants) { variants = choiceAuthorityService .getVariants( - values.get(x)); + values.get(x), collection); } if (StringUtils diff --git a/dspace-api/src/main/java/org/dspace/discovery/indexobject/ItemIndexFactoryImpl.java b/dspace-api/src/main/java/org/dspace/discovery/indexobject/ItemIndexFactoryImpl.java index 7f98131566..2a1008aaf9 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/indexobject/ItemIndexFactoryImpl.java +++ b/dspace-api/src/main/java/org/dspace/discovery/indexobject/ItemIndexFactoryImpl.java @@ -173,6 +173,8 @@ public class ItemIndexFactoryImpl extends DSpaceObjectIndexFactoryImpl discoveryConfigurations) throws SQLException, IOException { + // use the item service to retrieve the owning collection also for inprogress submission + Collection collection = (Collection) itemService.getParentObject(context, item); //Keep a list of our sort values which we added, sort values can only be added once List sortFieldsAdded = new ArrayList<>(); Map> searchFilters = null; @@ -359,7 +361,7 @@ public class ItemIndexFactoryImpl extends DSpaceObjectIndexFactoryImpl +

+ + + dc + contributor + author + + name + false + You must enter at least the author. + Enter the names of the authors of this item in the form Lastname, Firstname [i.e. Smith, Josh or Smith, J]. + + + + + person + affiliation + name + + onebox + false + + Enter the affiliation of the author as stated on the publication. + + +
+ +
+ + + dc + contributor + author + true + + onebox + Author field that can be associated with an authority providing suggestion + + + + + + dc + contributor + editor + false + + name + Editor field that can be associated with an authority providing the special name lookup + + + + + + dc + subject + true + + onebox + Subject field that can be associated with an authority providing lookup + + + +
diff --git a/dspace-api/src/test/java/org/dspace/content/ItemTest.java b/dspace-api/src/test/java/org/dspace/content/ItemTest.java index 8c3cfa5a04..494368230b 100644 --- a/dspace-api/src/test/java/org/dspace/content/ItemTest.java +++ b/dspace-api/src/test/java/org/dspace/content/ItemTest.java @@ -490,8 +490,8 @@ public class ItemTest extends AbstractDSpaceObjectTest { // Set the item to have two pieces of metadata for dc.type and dc2.type String dcType = "DC-TYPE"; String testType = "TEST-TYPE"; - itemService.addMetadata(context, it, "dc", "type", null, null, dcType, "accepted", 0); - itemService.addMetadata(context, it, "test", "type", null, null, testType, "accepted", 0); + itemService.addMetadata(context, it, "dc", "type", null, null, dcType); + itemService.addMetadata(context, it, "test", "type", null, null, testType); // Check that only one is returned when we ask for all dc.type values List values = itemService.getMetadata(it, "dc", "type", null, null); diff --git a/dspace-api/src/test/java/org/dspace/content/authority/DSpaceControlledVocabularyTest.java b/dspace-api/src/test/java/org/dspace/content/authority/DSpaceControlledVocabularyTest.java index 0d431a5a5b..77cf105dd4 100644 --- a/dspace-api/src/test/java/org/dspace/content/authority/DSpaceControlledVocabularyTest.java +++ b/dspace-api/src/test/java/org/dspace/content/authority/DSpaceControlledVocabularyTest.java @@ -78,7 +78,7 @@ public class DSpaceControlledVocabularyTest extends AbstractDSpaceTest { String text = "north 40"; Collection collection = null; int start = 0; - int limit = 0; + int limit = 10; String locale = null; // This "farm" Controlled Vocab is included in TestEnvironment data // (under /src/test/data/dspaceFolder/) and it should be auto-loaded @@ -86,8 +86,7 @@ public class DSpaceControlledVocabularyTest extends AbstractDSpaceTest { DSpaceControlledVocabulary instance = (DSpaceControlledVocabulary) CoreServiceFactory.getInstance().getPluginService().getNamedPlugin(Class.forName(PLUGIN_INTERFACE), "farm"); assertNotNull(instance); - Choices result = instance.getMatches(field, text, collection, start, - limit, locale); + Choices result = instance.getMatches(text, start, limit, locale); assertEquals("the farm::north 40", result.values[0].value); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java index 65eb303abd..9e14df2ec3 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java @@ -149,7 +149,7 @@ public class RestResourceController implements InitializingBean { * @return single DSpaceResource */ @RequestMapping(method = RequestMethod.GET, value = REGEX_REQUESTMAPPING_IDENTIFIER_AS_DIGIT) - public DSpaceResource findOne(@PathVariable String apiCategory, @PathVariable String model, + public HALResource findOne(@PathVariable String apiCategory, @PathVariable String model, @PathVariable Integer id) { return findOneInternal(apiCategory, model, id); } @@ -180,7 +180,7 @@ public class RestResourceController implements InitializingBean { * @return single DSpaceResource */ @RequestMapping(method = RequestMethod.GET, value = REGEX_REQUESTMAPPING_IDENTIFIER_AS_STRING_VERSION_STRONG) - public DSpaceResource findOne(@PathVariable String apiCategory, @PathVariable String model, + public HALResource findOne(@PathVariable String apiCategory, @PathVariable String model, @PathVariable String id) { return findOneInternal(apiCategory, model, id); } @@ -200,7 +200,7 @@ public class RestResourceController implements InitializingBean { * @return single DSpaceResource */ @RequestMapping(method = RequestMethod.GET, value = REGEX_REQUESTMAPPING_IDENTIFIER_AS_UUID) - public DSpaceResource findOne(@PathVariable String apiCategory, @PathVariable String model, + public HALResource findOne(@PathVariable String apiCategory, @PathVariable String model, @PathVariable UUID uuid) { return findOneInternal(apiCategory, model, uuid); } @@ -213,7 +213,7 @@ public class RestResourceController implements InitializingBean { * @param id Identifier from request * @return single DSpaceResource */ - private DSpaceResource findOneInternal(String apiCategory, + private HALResource findOneInternal(String apiCategory, String model, ID id) { DSpaceRestRepository repository = utils.getResourceRepository(apiCategory, model); Optional modelObject = Optional.empty(); @@ -799,7 +799,7 @@ public class RestResourceController implements InitializingBean { Method linkMethod = utils.requireMethod(linkRepository.getClass(), linkRest.method()); try { if (Page.class.isAssignableFrom(linkMethod.getReturnType())) { - Page pageResult = (Page) linkMethod + Page pageResult = (Page) linkMethod .invoke(linkRepository, request, uuid, page, utils.obtainProjection()); if (pageResult == null) { @@ -823,8 +823,8 @@ public class RestResourceController implements InitializingBean { return new EntityModel(new EmbeddedPage(link.getHref(), pageResult.map(converter::toResource), null, subpath)); } else { - RestModel object = (RestModel) linkMethod.invoke(linkRepository, request, uuid, page, - utils.obtainProjection()); + RestModel object = (RestModel) linkMethod.invoke(linkRepository, request, + uuid, page, utils.obtainProjection()); if (object == null) { response.setStatus(HttpServletResponse.SC_NO_CONTENT); return null; @@ -846,7 +846,7 @@ public class RestResourceController implements InitializingBean { throw new RuntimeException(e); } } - RestAddressableModel modelObject = repository.findById(uuid).orElse(null); + RestModel modelObject = repository.findById(uuid).orElse(null); if (modelObject == null) { throw new ResourceNotFoundException(apiCategory + "." + model + " with id: " + uuid + " not found"); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionFormConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionFormConverter.java index f8f34612eb..339f601dc4 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionFormConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionFormConverter.java @@ -115,14 +115,16 @@ public class SubmissionFormConverter implements DSpaceConverter getModelClass() { return DCInputSet.class; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/AuthorityEntryRestConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/VocabularyEntryDetailsRestConverter.java similarity index 74% rename from dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/AuthorityEntryRestConverter.java rename to dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/VocabularyEntryDetailsRestConverter.java index aa5e196dc1..358a71455d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/AuthorityEntryRestConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/VocabularyEntryDetailsRestConverter.java @@ -7,7 +7,7 @@ */ package org.dspace.app.rest.converter; -import org.dspace.app.rest.model.AuthorityEntryRest; +import org.dspace.app.rest.model.VocabularyEntryDetailsRest; import org.dspace.app.rest.projection.Projection; import org.dspace.app.rest.utils.AuthorityUtils; import org.dspace.content.authority.Choice; @@ -22,16 +22,17 @@ import org.springframework.stereotype.Component; * @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it) */ @Component -public class AuthorityEntryRestConverter implements DSpaceConverter { +public class VocabularyEntryDetailsRestConverter implements DSpaceConverter { @Override - public AuthorityEntryRest convert(Choice choice, Projection projection) { - AuthorityEntryRest entry = new AuthorityEntryRest(); + public VocabularyEntryDetailsRest convert(Choice choice, Projection projection) { + VocabularyEntryDetailsRest entry = new VocabularyEntryDetailsRest(); entry.setProjection(projection); entry.setValue(choice.value); entry.setDisplay(choice.label); entry.setId(choice.authority); entry.setOtherInformation(choice.extras); + entry.setSelectable(choice.selectable); return entry; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/AuthorityRestConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/VocabularyRestConverter.java similarity index 67% rename from dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/AuthorityRestConverter.java rename to dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/VocabularyRestConverter.java index 7e78ef7f14..5dcb05a23e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/AuthorityRestConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/VocabularyRestConverter.java @@ -7,7 +7,7 @@ */ package org.dspace.app.rest.converter; -import org.dspace.app.rest.model.AuthorityRest; +import org.dspace.app.rest.model.VocabularyRest; import org.dspace.app.rest.projection.Projection; import org.dspace.app.rest.utils.AuthorityUtils; import org.dspace.content.authority.ChoiceAuthority; @@ -23,15 +23,15 @@ import org.springframework.stereotype.Component; * @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it) */ @Component -public class AuthorityRestConverter implements DSpaceConverter { +public class VocabularyRestConverter implements DSpaceConverter { @Override - public AuthorityRest convert(ChoiceAuthority step, Projection projection) { - AuthorityRest authorityRest = new AuthorityRest(); + public VocabularyRest convert(ChoiceAuthority authority, Projection projection) { + VocabularyRest authorityRest = new VocabularyRest(); authorityRest.setProjection(projection); - authorityRest.setHierarchical(step.isHierarchical()); - authorityRest.setScrollable(step.isScrollable()); - authorityRest.setIdentifier(step.hasIdentifier()); + authorityRest.setHierarchical(authority.isHierarchical()); + authorityRest.setScrollable(authority.isScrollable()); + authorityRest.setPreloadLevel(authority.getPreloadLevel()); return authorityRest; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/LinkNotFoundException.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/LinkNotFoundException.java new file mode 100644 index 0000000000..5710b7a176 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/LinkNotFoundException.java @@ -0,0 +1,30 @@ +/** + * 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.app.rest.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * This is the exception to capture details about a not existing linked resource + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + */ +@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "This link is not found in the system") +public class LinkNotFoundException extends RuntimeException { + String apiCategory; + String model; + String id; + + public LinkNotFoundException(String apiCategory, String model, String id) { + this.apiCategory = apiCategory; + this.model = model; + this.id = id; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/AuthorityEntryHalLinkFactory.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/AuthorityEntryHalLinkFactory.java deleted file mode 100644 index e24d70a526..0000000000 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/AuthorityEntryHalLinkFactory.java +++ /dev/null @@ -1,66 +0,0 @@ -/** - * 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.app.rest.link; - -import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; - -import java.util.LinkedList; - -import org.atteo.evo.inflector.English; -import org.dspace.app.rest.RestResourceController; -import org.dspace.app.rest.model.AuthorityEntryRest; -import org.dspace.app.rest.model.AuthorityRest; -import org.dspace.app.rest.model.hateoas.AuthorityEntryResource; -import org.dspace.app.rest.utils.AuthorityUtils; -import org.springframework.data.domain.Pageable; -import org.springframework.hateoas.IanaLinkRelations; -import org.springframework.hateoas.Link; -import org.springframework.stereotype.Component; -import org.springframework.web.util.UriComponentsBuilder; - -/** - * This class' purpose is to provide a factory to add links to the AuthorityEntryResource. The addLinks factory will - * be called - * from the HalLinkService class addLinks method. - */ -@Component -public class AuthorityEntryHalLinkFactory extends HalLinkFactory { - - protected void addLinks(final AuthorityEntryResource halResource, final Pageable pageable, - final LinkedList list) throws Exception { - AuthorityEntryRest entry = halResource.getContent(); - - if (entry.getOtherInformation() != null) { - if (entry.getOtherInformation().containsKey(AuthorityUtils.RESERVED_KEYMAP_PARENT)) { - UriComponentsBuilder uriComponentsBuilder = linkTo( - getMethodOn(AuthorityRest.CATEGORY, AuthorityRest.NAME) - .findRel(null, null, AuthorityRest.CATEGORY, - English.plural(AuthorityRest.NAME), - entry.getAuthorityName() + "/" + AuthorityRest.ENTRY, - entry.getOtherInformation().get(AuthorityUtils.RESERVED_KEYMAP_PARENT), null, null)) - .toUriComponentsBuilder(); - - list.add(buildLink(AuthorityUtils.RESERVED_KEYMAP_PARENT, uriComponentsBuilder.build().toString())); - } - } - String selfLinkString = linkTo( - getMethodOn().findOne(entry.getCategory(), English.plural(entry.getType()), entry.getAuthorityName())) - .toUriComponentsBuilder().build().toString() + "/entryValues/" + entry.getId(); - list.add(buildLink(IanaLinkRelations.SELF.value(), selfLinkString)); - } - - protected Class getControllerClass() { - return RestResourceController.class; - } - - protected Class getResourceClass() { - return AuthorityEntryResource.class; - } - -} - diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionFormInputTypeRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionFormInputTypeRest.java index 594d715b22..ff5481443b 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionFormInputTypeRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionFormInputTypeRest.java @@ -21,7 +21,6 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include; public class SubmissionFormInputTypeRest { private String type; private String regex; - private AuthorityRest authority; public String getType() { return type; @@ -39,11 +38,4 @@ public class SubmissionFormInputTypeRest { this.regex = regex; } - public AuthorityRest getAuthority() { - return authority; - } - - public void setAuthority(AuthorityRest authority) { - this.authority = authority; - } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/VocabularyEntryDetailsRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/VocabularyEntryDetailsRest.java new file mode 100644 index 0000000000..42644c8c85 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/VocabularyEntryDetailsRest.java @@ -0,0 +1,104 @@ +/** + * 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.app.rest.model; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.dspace.app.rest.RestResourceController; + +/** + * The Vocabulary Entry Details REST Resource + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + */ +@LinksRest(links = { + @LinkRest(name = VocabularyEntryDetailsRest.PARENT, method = "getParent"), + @LinkRest(name = VocabularyEntryDetailsRest.CHILDREN, method = "getChildren") + }) +public class VocabularyEntryDetailsRest extends BaseObjectRest { + public static final String NAME = "vocabularyEntryDetail"; + public static final String PARENT = "parent"; + public static final String CHILDREN = "children"; + private String display; + private String value; + private Map otherInformation; + private boolean selectable; + @JsonIgnore + private boolean inHierarchicalVocabulary = false; + + @JsonIgnore + private String vocabularyName; + + public String getDisplay() { + return display; + } + + public void setDisplay(String value) { + this.display = value; + } + + public Map getOtherInformation() { + return otherInformation; + } + + public void setOtherInformation(Map otherInformation) { + this.otherInformation = otherInformation; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public static String getName() { + return NAME; + } + + public String getVocabularyName() { + return vocabularyName; + } + + public void setVocabularyName(String vocabularyName) { + this.vocabularyName = vocabularyName; + } + + @Override + public String getCategory() { + return VocabularyRest.CATEGORY; + } + + @Override + public String getType() { + return VocabularyEntryDetailsRest.NAME; + } + + @Override + public Class getController() { + return RestResourceController.class; + } + + public Boolean isSelectable() { + return selectable; + } + + public void setSelectable(Boolean selectable) { + this.selectable = selectable; + } + + public void setInHierarchicalVocabulary(boolean isInHierarchicalVocabulary) { + this.inHierarchicalVocabulary = isInHierarchicalVocabulary; + } + + public boolean isInHierarchicalVocabulary() { + return inHierarchicalVocabulary; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthorityEntryRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/VocabularyEntryRest.java similarity index 53% rename from dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthorityEntryRest.java rename to dspace-server-webapp/src/main/java/org/dspace/app/rest/model/VocabularyEntryRest.java index 9fcc01d972..713d4c5209 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthorityEntryRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/VocabularyEntryRest.java @@ -10,30 +10,28 @@ package org.dspace.app.rest.model; import java.util.Map; import com.fasterxml.jackson.annotation.JsonIgnore; -import org.dspace.app.rest.RestResourceController; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; /** - * The Authority Entry REST Resource + * An entry in a Vocabulary * * @author Andrea Bollini (andrea.bollini at 4science.it) */ -public class AuthorityEntryRest extends RestAddressableModel { - public static final String NAME = "authorityEntry"; - private String id; +public class VocabularyEntryRest implements RestModel { + public static final String NAME = "vocabularyEntry"; + + @JsonInclude(Include.NON_NULL) + private String authority; private String display; private String value; private Map otherInformation; + /** + * The Vocabulary Entry Details resource if available related to this entry + */ @JsonIgnore - private String authorityName; - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } + private VocabularyEntryDetailsRest vocabularyEntryDetailsRest; public String getDisplay() { return display; @@ -59,31 +57,24 @@ public class AuthorityEntryRest extends RestAddressableModel { this.value = value; } - public static String getName() { - return NAME; + public void setAuthority(String authority) { + this.authority = authority; } - public String getAuthorityName() { - return authorityName; + public String getAuthority() { + return authority; } - public void setAuthorityName(String authorityName) { - this.authorityName = authorityName; + public void setVocabularyEntryDetailsRest(VocabularyEntryDetailsRest vocabularyEntryDetailsRest) { + this.vocabularyEntryDetailsRest = vocabularyEntryDetailsRest; } - @Override - public String getCategory() { - return AuthorityRest.CATEGORY; + public VocabularyEntryDetailsRest getVocabularyEntryDetailsRest() { + return vocabularyEntryDetailsRest; } @Override public String getType() { - return AuthorityRest.NAME; + return VocabularyEntryRest.NAME; } - - @Override - public Class getController() { - return RestResourceController.class; - } - } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthorityRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/VocabularyRest.java similarity index 68% rename from dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthorityRest.java rename to dspace-server-webapp/src/main/java/org/dspace/app/rest/model/VocabularyRest.java index 3245e6f877..cc848b945b 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthorityRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/VocabularyRest.java @@ -10,25 +10,20 @@ package org.dspace.app.rest.model; import org.dspace.app.rest.RestResourceController; /** - * The authority REST resource + * The vocabulary REST resource * * @author Andrea Bollini (andrea.bollini at 4science.it) */ @LinksRest(links = { - @LinkRest(name = AuthorityRest.ENTRIES, - method = "query" + @LinkRest(name = VocabularyRest.ENTRIES, + method = "filter" ), - @LinkRest( - name = AuthorityRest.ENTRY, - method = "getResource" - ) }) -public class AuthorityRest extends BaseObjectRest { +public class VocabularyRest extends BaseObjectRest { - public static final String NAME = "authority"; - public static final String CATEGORY = RestAddressableModel.INTEGRATION; + public static final String NAME = "vocabulary"; + public static final String CATEGORY = RestAddressableModel.SUBMISSION; public static final String ENTRIES = "entries"; - public static final String ENTRY = "entryValues"; private String name; @@ -36,7 +31,7 @@ public class AuthorityRest extends BaseObjectRest { private boolean hierarchical; - private boolean identifier; + private Integer preloadLevel; @Override public String getId() { @@ -67,6 +62,14 @@ public class AuthorityRest extends BaseObjectRest { this.hierarchical = hierarchical; } + public Integer getPreloadLevel() { + return preloadLevel; + } + + public void setPreloadLevel(Integer preloadLevel) { + this.preloadLevel = preloadLevel; + } + @Override public String getType() { return NAME; @@ -81,12 +84,4 @@ public class AuthorityRest extends BaseObjectRest { public String getCategory() { return CATEGORY; } - - public boolean hasIdentifier() { - return identifier; - } - - public void setIdentifier(boolean identifier) { - this.identifier = identifier; - } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/AuthorityEntryResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/AuthorityEntryResource.java deleted file mode 100644 index c99ebd6f2e..0000000000 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/AuthorityEntryResource.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * 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.app.rest.model.hateoas; - -import org.dspace.app.rest.model.AuthorityEntryRest; -import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource; - -/** - * Authority Rest HAL Resource. The HAL Resource wraps the REST Resource adding - * support for the links and embedded resources - * - * @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it) - */ -@RelNameDSpaceResource(AuthorityEntryRest.NAME) -public class AuthorityEntryResource extends HALResource { - - - public AuthorityEntryResource(AuthorityEntryRest entry) { - super(entry); - } -} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/VocabularyEntryDetailsResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/VocabularyEntryDetailsResource.java new file mode 100644 index 0000000000..0467b29cef --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/VocabularyEntryDetailsResource.java @@ -0,0 +1,30 @@ +/** + * 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.app.rest.model.hateoas; + +import org.dspace.app.rest.model.VocabularyEntryDetailsRest; +import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource; +import org.dspace.app.rest.utils.Utils; + +/** + * Vocabulary Entry Details Rest HAL Resource. The HAL Resource wraps the REST Resource adding + * support for the links and embedded resources + * + * @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it) + */ +@RelNameDSpaceResource(VocabularyEntryDetailsRest.NAME) +public class VocabularyEntryDetailsResource extends DSpaceResource { + + public VocabularyEntryDetailsResource(VocabularyEntryDetailsRest entry, Utils utils) { + super(entry, utils); + if (entry.isInHierarchicalVocabulary()) { + add(utils.linkToSubResource(entry, VocabularyEntryDetailsRest.PARENT)); + add(utils.linkToSubResource(entry, VocabularyEntryDetailsRest.CHILDREN)); + } + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/VocabularyEntryResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/VocabularyEntryResource.java new file mode 100644 index 0000000000..c29baa9fcd --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/VocabularyEntryResource.java @@ -0,0 +1,24 @@ +/** + * 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.app.rest.model.hateoas; + +import org.dspace.app.rest.model.VocabularyEntryRest; +import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource; + +/** + * Vocabulary Entry Rest HAL Resource. The HAL Resource wraps the REST Resource + * adding support for the links and embedded resources + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.it) + */ +@RelNameDSpaceResource(VocabularyEntryRest.NAME) +public class VocabularyEntryResource extends HALResource { + public VocabularyEntryResource(VocabularyEntryRest sd) { + super(sd); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/AuthorityResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/VocabularyResource.java similarity index 61% rename from dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/AuthorityResource.java rename to dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/VocabularyResource.java index 0e153097b4..4a2ec01d33 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/AuthorityResource.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/VocabularyResource.java @@ -7,7 +7,7 @@ */ package org.dspace.app.rest.model.hateoas; -import org.dspace.app.rest.model.AuthorityRest; +import org.dspace.app.rest.model.VocabularyRest; import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource; import org.dspace.app.rest.utils.Utils; @@ -17,13 +17,10 @@ import org.dspace.app.rest.utils.Utils; * * @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it) */ -@RelNameDSpaceResource(AuthorityRest.NAME) -public class AuthorityResource extends DSpaceResource { - public AuthorityResource(AuthorityRest sd, Utils utils) { +@RelNameDSpaceResource(VocabularyRest.NAME) +public class VocabularyResource extends DSpaceResource { + public VocabularyResource(VocabularyRest sd, Utils utils) { super(sd, utils); - if (sd.hasIdentifier()) { - add(utils.linkToSubResource(sd, AuthorityRest.ENTRY)); - } - add(utils.linkToSubResource(sd, AuthorityRest.ENTRIES)); + add(utils.linkToSubResource(sd, VocabularyRest.ENTRIES)); } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/submit/SelectableMetadata.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/submit/SelectableMetadata.java index 06f2cfc459..a203bb2721 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/submit/SelectableMetadata.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/submit/SelectableMetadata.java @@ -7,6 +7,9 @@ */ package org.dspace.app.rest.model.submit; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + /** * The SelectableMetadata REST Resource. It is not addressable directly, only * used as inline object in the InputForm resource. @@ -24,7 +27,9 @@ package org.dspace.app.rest.model.submit; public class SelectableMetadata { private String metadata; private String label; - private String authority; + @JsonInclude(Include.NON_NULL) + private String controlledVocabulary; + @JsonInclude(Include.NON_NULL) private Boolean closed = false; public String getMetadata() { @@ -43,12 +48,12 @@ public class SelectableMetadata { this.label = label; } - public void setAuthority(String authority) { - this.authority = authority; + public void setControlledVocabulary(String vocabularyName) { + this.controlledVocabulary = vocabularyName; } - public String getAuthority() { - return authority; + public String getControlledVocabulary() { + return controlledVocabulary; } public Boolean isClosed() { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AuthorityEntryLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AuthorityEntryLinkRepository.java deleted file mode 100644 index 0c3ec16299..0000000000 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AuthorityEntryLinkRepository.java +++ /dev/null @@ -1,82 +0,0 @@ -/** - * 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.app.rest.repository; - -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; -import javax.annotation.Nullable; -import javax.servlet.http.HttpServletRequest; - -import org.apache.commons.lang3.StringUtils; -import org.dspace.app.rest.model.AuthorityEntryRest; -import org.dspace.app.rest.model.AuthorityRest; -import org.dspace.app.rest.projection.Projection; -import org.dspace.app.rest.utils.AuthorityUtils; -import org.dspace.content.Collection; -import org.dspace.content.authority.Choice; -import org.dspace.content.authority.Choices; -import org.dspace.content.authority.service.ChoiceAuthorityService; -import org.dspace.content.service.CollectionService; -import org.dspace.core.Context; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.stereotype.Component; - -/** - * Controller for exposition of authority services - * - * @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it) - */ -@Component(AuthorityRest.CATEGORY + "." + AuthorityRest.NAME + "." + AuthorityRest.ENTRIES) -public class AuthorityEntryLinkRepository extends AbstractDSpaceRestRepository - implements LinkRestRepository { - - @Autowired - private ChoiceAuthorityService cas; - - @Autowired - private CollectionService cs; - - @Autowired - private AuthorityUtils authorityUtils; - - @PreAuthorize("hasAuthority('AUTHENTICATED')") - public Page query(@Nullable HttpServletRequest request, String name, - @Nullable Pageable optionalPageable, Projection projection) { - Context context = obtainContext(); - String query = request == null ? null : request.getParameter("query"); - String metadata = request == null ? null : request.getParameter("metadata"); - String uuidCollectìon = request == null ? null : request.getParameter("uuid"); - Collection collection = null; - if (StringUtils.isNotBlank(uuidCollectìon)) { - try { - collection = cs.find(context, UUID.fromString(uuidCollectìon)); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - List results = new ArrayList<>(); - Pageable pageable = utils.getPageable(optionalPageable); - if (StringUtils.isNotBlank(metadata)) { - String[] tokens = org.dspace.core.Utils.tokenize(metadata); - String fieldKey = org.dspace.core.Utils.standardize(tokens[0], tokens[1], tokens[2], "_"); - Choices choices = cas.getMatches(fieldKey, query, collection, Math.toIntExact(pageable.getOffset()), - pageable.getPageSize(), - context.getCurrentLocale().toString()); - for (Choice value : choices.values) { - results.add(authorityUtils.convertEntry(value, name, projection)); - } - } - return new PageImpl<>(results, pageable, results.size()); - } -} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AuthorityEntryValueLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AuthorityEntryValueLinkRepository.java deleted file mode 100644 index c2e3c557d4..0000000000 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AuthorityEntryValueLinkRepository.java +++ /dev/null @@ -1,60 +0,0 @@ -/** - * 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.app.rest.repository; - -import javax.servlet.http.HttpServletRequest; - -import org.dspace.app.rest.model.AuthorityEntryRest; -import org.dspace.app.rest.model.AuthorityRest; -import org.dspace.app.rest.projection.Projection; -import org.dspace.app.rest.utils.AuthorityUtils; -import org.dspace.content.authority.Choice; -import org.dspace.content.authority.ChoiceAuthority; -import org.dspace.content.authority.service.ChoiceAuthorityService; -import org.dspace.core.Context; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Pageable; -import org.springframework.data.rest.webmvc.ResourceNotFoundException; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.stereotype.Component; - -/** - * Controller for exposition of authority services - * - * @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it) - */ -@Component(AuthorityRest.CATEGORY + "." + AuthorityRest.NAME + "." + AuthorityRest.ENTRY) -public class AuthorityEntryValueLinkRepository extends AbstractDSpaceRestRepository - implements LinkRestRepository { - - @Autowired - private ChoiceAuthorityService cas; - - @Autowired - private AuthorityUtils authorityUtils; - - @PreAuthorize("hasAuthority('AUTHENTICATED')") - public AuthorityEntryRest getResource(HttpServletRequest request, String name, String relId, - Pageable pageable, Projection projection) { - Context context = obtainContext(); - ChoiceAuthority choiceAuthority = cas.getChoiceAuthorityByAuthorityName(name); - Choice choice = choiceAuthority.getChoice(null, relId, context.getCurrentLocale().toString()); - if (choice == null) { - throw new ResourceNotFoundException("The authority was not found"); - } - return authorityUtils.convertEntry(choice, name, projection); - } - - /** - * Not embeddable because this is not currently a pageable subresource. - */ - @Override - public boolean isEmbeddableRelation(Object data, String name) { - return false; - } -} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AuthorityRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AuthorityRestRepository.java deleted file mode 100644 index d5dda5a0bc..0000000000 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AuthorityRestRepository.java +++ /dev/null @@ -1,82 +0,0 @@ -/** - * 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.app.rest.repository; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Set; - -import org.dspace.app.rest.DiscoverableEndpointsService; -import org.dspace.app.rest.model.AuthorityRest; -import org.dspace.app.rest.model.AuthorizationRest; -import org.dspace.app.rest.projection.Projection; -import org.dspace.app.rest.utils.AuthorityUtils; -import org.dspace.content.authority.ChoiceAuthority; -import org.dspace.content.authority.service.ChoiceAuthorityService; -import org.dspace.core.Context; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; -import org.springframework.hateoas.Link; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.stereotype.Component; - -/** - * Controller for exposition of authority services - * - * @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it) - */ -@Component(AuthorityRest.CATEGORY + "." + AuthorityRest.NAME) -public class AuthorityRestRepository extends DSpaceRestRepository - implements InitializingBean { - - @Autowired - private ChoiceAuthorityService cas; - - @Autowired - private AuthorityUtils authorityUtils; - - @Autowired - DiscoverableEndpointsService discoverableEndpointsService; - - @PreAuthorize("hasAuthority('AUTHENTICATED')") - @Override - public AuthorityRest findOne(Context context, String name) { - ChoiceAuthority source = cas.getChoiceAuthorityByAuthorityName(name); - return authorityUtils.convertAuthority(source, name, utils.obtainProjection()); - } - - @PreAuthorize("hasAuthority('AUTHENTICATED')") - @Override - public Page findAll(Context context, Pageable pageable) { - Set authoritiesName = cas.getChoiceAuthoritiesNames(); - List results = new ArrayList<>(); - Projection projection = utils.obtainProjection(); - for (String authorityName : authoritiesName) { - ChoiceAuthority source = cas.getChoiceAuthorityByAuthorityName(authorityName); - AuthorityRest result = authorityUtils.convertAuthority(source, authorityName, projection); - results.add(result); - } - return new PageImpl<>(results, pageable, results.size()); - } - - @Override - public Class getDomainClass() { - return AuthorityRest.class; - } - - @Override - public void afterPropertiesSet() throws Exception { - discoverableEndpointsService.register(this, Arrays.asList( - new Link("/api/" + AuthorizationRest.CATEGORY + "/" + AuthorizationRest.NAME + "/search", - AuthorizationRest.NAME + "-search"))); - } -} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/VocabularyEntryDetailsChildrenLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/VocabularyEntryDetailsChildrenLinkRepository.java new file mode 100644 index 0000000000..044710d25b --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/VocabularyEntryDetailsChildrenLinkRepository.java @@ -0,0 +1,77 @@ +/** + * 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.app.rest.repository; + +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.app.rest.exception.LinkNotFoundException; +import org.dspace.app.rest.model.VocabularyEntryDetailsRest; +import org.dspace.app.rest.model.VocabularyRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.app.rest.utils.AuthorityUtils; +import org.dspace.content.authority.Choice; +import org.dspace.content.authority.ChoiceAuthority; +import org.dspace.content.authority.Choices; +import org.dspace.content.authority.service.ChoiceAuthorityService; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Component; + +/** + * Link repository to expose the parent of a vocabulary entry details in an hierarchical vocabulary + * + * @author Mykhaylo Boychuk (4Science.it) + */ +@Component(VocabularyRest.CATEGORY + "." + VocabularyEntryDetailsRest.NAME + "." + VocabularyEntryDetailsRest.CHILDREN) +public class VocabularyEntryDetailsChildrenLinkRepository extends AbstractDSpaceRestRepository + implements LinkRestRepository { + + @Autowired + private ChoiceAuthorityService choiceAuthorityService; + + @Autowired + private AuthorityUtils authorityUtils; + + @PreAuthorize("hasAuthority('AUTHENTICATED')") + public Page getChildren(@Nullable HttpServletRequest request, String name, + @Nullable Pageable optionalPageable, Projection projection) { + + Context context = obtainContext(); + String[] parts = StringUtils.split(name, ":", 2); + if (parts.length != 2) { + return null; + } + String vocabularyName = parts[0]; + String id = parts[1]; + Pageable pageable = utils.getPageable(optionalPageable); + List results = new ArrayList(); + ChoiceAuthority authority = choiceAuthorityService.getChoiceAuthorityByAuthorityName(vocabularyName); + if (StringUtils.isNotBlank(id) && authority.isHierarchical()) { + Choices choices = choiceAuthorityService.getChoicesByParent(vocabularyName, id, (int) pageable.getOffset(), + pageable.getPageSize(), context.getCurrentLocale().toString()); + for (Choice value : choices.values) { + results.add(authorityUtils.convertEntryDetails(value, vocabularyName, authority.isHierarchical(), + utils.obtainProjection())); + } + Page resources = new PageImpl(results, pageable, + choices.total); + return resources; + } else { + throw new LinkNotFoundException(VocabularyRest.CATEGORY, VocabularyEntryDetailsRest.NAME, name); + } + } +} + diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/VocabularyEntryDetailsParentLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/VocabularyEntryDetailsParentLinkRepository.java new file mode 100644 index 0000000000..379928d9cc --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/VocabularyEntryDetailsParentLinkRepository.java @@ -0,0 +1,64 @@ +/** + * 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.app.rest.repository; + +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.NotFoundException; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.app.rest.model.VocabularyEntryDetailsRest; +import org.dspace.app.rest.model.VocabularyRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.app.rest.utils.AuthorityUtils; +import org.dspace.content.authority.Choice; +import org.dspace.content.authority.ChoiceAuthority; +import org.dspace.content.authority.service.ChoiceAuthorityService; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Component; + +/** + * Link repository to expose the parent of a vocabulary entry details in an hierarchical vocabulary + * + * @author Mykhaylo Boychuk ($science.it) + */ +@Component(VocabularyRest.CATEGORY + "." + VocabularyEntryDetailsRest.NAME + "." + VocabularyEntryDetailsRest.PARENT) +public class VocabularyEntryDetailsParentLinkRepository extends AbstractDSpaceRestRepository + implements LinkRestRepository { + + @Autowired + private ChoiceAuthorityService choiceAuthorityService; + + @Autowired + private AuthorityUtils authorityUtils; + + @PreAuthorize("hasAuthority('AUTHENTICATED')") + public VocabularyEntryDetailsRest getParent(@Nullable HttpServletRequest request, String name, + @Nullable Pageable optionalPageable, Projection projection) { + Context context = obtainContext(); + String[] parts = StringUtils.split(name, ":", 2); + if (parts.length != 2) { + return null; + } + String vocabularyName = parts[0]; + String id = parts[1]; + + ChoiceAuthority authority = choiceAuthorityService.getChoiceAuthorityByAuthorityName(vocabularyName); + Choice choice = null; + if (StringUtils.isNotBlank(id) && authority != null && authority.isHierarchical()) { + choice = choiceAuthorityService.getParentChoice(vocabularyName, id, context.getCurrentLocale().toString()); + } else { + throw new NotFoundException(); + } + return authorityUtils.convertEntryDetails(choice, vocabularyName, authority.isHierarchical(), + utils.obtainProjection()); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/VocabularyEntryDetailsRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/VocabularyEntryDetailsRestRepository.java new file mode 100644 index 0000000000..26e43cac8b --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/VocabularyEntryDetailsRestRepository.java @@ -0,0 +1,111 @@ +/** + * 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.app.rest.repository; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.atteo.evo.inflector.English; +import org.dspace.app.rest.DiscoverableEndpointsService; +import org.dspace.app.rest.Parameter; +import org.dspace.app.rest.SearchRestMethod; +import org.dspace.app.rest.exception.LinkNotFoundException; +import org.dspace.app.rest.exception.RepositoryMethodNotImplementedException; +import org.dspace.app.rest.model.ResourcePolicyRest; +import org.dspace.app.rest.model.VocabularyEntryDetailsRest; +import org.dspace.app.rest.model.VocabularyRest; +import org.dspace.app.rest.utils.AuthorityUtils; +import org.dspace.content.authority.Choice; +import org.dspace.content.authority.ChoiceAuthority; +import org.dspace.content.authority.Choices; +import org.dspace.content.authority.service.ChoiceAuthorityService; +import org.dspace.core.Context; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.hateoas.Link; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Component; + +/** + * Controller for exposition of vocabularies entry details for the submission + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + */ +@Component(VocabularyRest.CATEGORY + "." + VocabularyEntryDetailsRest.NAME) +public class VocabularyEntryDetailsRestRepository extends DSpaceRestRepository + implements InitializingBean { + + @Autowired + private ChoiceAuthorityService cas; + + @Autowired + private AuthorityUtils authorityUtils; + + @Autowired + private DiscoverableEndpointsService discoverableEndpointsService; + + @Override + public void afterPropertiesSet() throws Exception { + String models = English.plural(VocabularyEntryDetailsRest.NAME); + discoverableEndpointsService.register(this, Arrays.asList( + new Link("/api/" + VocabularyRest.CATEGORY + "/" + models + "/search", + models + "-search"))); + } + + @PreAuthorize("hasAuthority('AUTHENTICATED')") + @Override + public Page findAll(Context context, Pageable pageable) { + throw new RepositoryMethodNotImplementedException(ResourcePolicyRest.NAME, "findAll"); + } + + @PreAuthorize("hasAuthority('AUTHENTICATED')") + @Override + public VocabularyEntryDetailsRest findOne(Context context, String name) { + String[] parts = StringUtils.split(name, ":", 2); + if (parts.length != 2) { + return null; + } + String vocabularyName = parts[0]; + String vocabularyId = parts[1]; + ChoiceAuthority source = cas.getChoiceAuthorityByAuthorityName(vocabularyName); + Choice choice = source.getChoice(vocabularyId, context.getCurrentLocale().toString()); + return authorityUtils.convertEntryDetails(choice, vocabularyName, source.isHierarchical(), + utils.obtainProjection()); + } + + @SearchRestMethod(name = "top") + @PreAuthorize("hasAuthority('AUTHENTICATED')") + public Page findAllTop(@Parameter(value = "vocabulary", required = true) + String vocabularyId, Pageable pageable) { + Context context = obtainContext(); + List results = new ArrayList(); + ChoiceAuthority source = cas.getChoiceAuthorityByAuthorityName(vocabularyId); + if (source.isHierarchical()) { + Choices choices = cas.getTopChoices(vocabularyId, (int)pageable.getOffset(), pageable.getPageSize(), + context.getCurrentLocale().toString()); + for (Choice value : choices.values) { + results.add(authorityUtils.convertEntryDetails(value, vocabularyId, source.isHierarchical(), + utils.obtainProjection())); + } + Page resources = new PageImpl(results, pageable, + choices.total); + return resources; + } + throw new LinkNotFoundException(VocabularyRest.CATEGORY, VocabularyEntryDetailsRest.NAME, vocabularyId); + } + + @Override + public Class getDomainClass() { + return VocabularyEntryDetailsRest.class; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/VocabularyEntryLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/VocabularyEntryLinkRepository.java new file mode 100644 index 0000000000..9d75ef87c3 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/VocabularyEntryLinkRepository.java @@ -0,0 +1,97 @@ +/** + * 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.app.rest.repository; + +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.VocabularyEntryRest; +import org.dspace.app.rest.model.VocabularyRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.app.rest.utils.AuthorityUtils; +import org.dspace.content.authority.Choice; +import org.dspace.content.authority.ChoiceAuthority; +import org.dspace.content.authority.Choices; +import org.dspace.content.authority.service.ChoiceAuthorityService; +import org.dspace.content.service.CollectionService; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Component; + +/** + * Controller for exposition of authority services + * + * @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it) + */ +@Component(VocabularyRest.CATEGORY + "." + VocabularyRest.NAME + "." + VocabularyRest.ENTRIES) +public class VocabularyEntryLinkRepository extends AbstractDSpaceRestRepository + implements LinkRestRepository { + + @Autowired + private ChoiceAuthorityService cas; + + @Autowired + private CollectionService cs; + + @Autowired + private AuthorityUtils authorityUtils; + + @PreAuthorize("hasAuthority('AUTHENTICATED')") + public Page filter(@Nullable HttpServletRequest request, String name, + @Nullable Pageable optionalPageable, Projection projection) { + Context context = obtainContext(); + String exact = request == null ? null : request.getParameter("exact"); + String filter = request == null ? null : request.getParameter("filter"); + String entryID = request == null ? null : request.getParameter("entryID"); + + if (StringUtils.isNotBlank(filter) && StringUtils.isNotBlank(entryID)) { + throw new IllegalArgumentException("the filter and entryID parameters are mutually exclusive"); + } + + Pageable pageable = utils.getPageable(optionalPageable); + List results = new ArrayList<>(); + ChoiceAuthority ca = cas.getChoiceAuthorityByAuthorityName(name); + if (ca == null) { + throw new ResourceNotFoundException("the vocabulary named " + name + "doesn't exist"); + } + if (!ca.isScrollable() && StringUtils.isBlank(filter) && StringUtils.isBlank(entryID)) { + throw new UnprocessableEntityException( + "one of filter or entryID parameter is required for not scrollable vocabularies"); + } + Choices choices = null; + if (BooleanUtils.toBoolean(exact)) { + choices = ca.getBestMatch(filter, context.getCurrentLocale().toString()); + } else if (StringUtils.isNotBlank(entryID)) { + Choice choice = ca.getChoice(entryID, + context.getCurrentLocale().toString()); + if (choice != null) { + choices = new Choices(new Choice[] {choice}, 0, 1, Choices.CF_ACCEPTED, false); + } else { + choices = new Choices(false); + } + } else { + choices = ca.getMatches(filter, Math.toIntExact(pageable.getOffset()), + pageable.getPageSize(), context.getCurrentLocale().toString()); + } + boolean storeAuthority = ca.storeAuthorityInMetadata(); + for (Choice value : choices.values) { + results.add(authorityUtils.convertEntry(value, name, storeAuthority, projection)); + } + return new PageImpl<>(results, pageable, choices.total); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/VocabularyRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/VocabularyRestRepository.java new file mode 100644 index 0000000000..dcdf71186b --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/VocabularyRestRepository.java @@ -0,0 +1,112 @@ +/** + * 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.app.rest.repository; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import org.dspace.app.rest.Parameter; +import org.dspace.app.rest.SearchRestMethod; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.VocabularyRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.app.rest.utils.AuthorityUtils; +import org.dspace.content.Collection; +import org.dspace.content.MetadataField; +import org.dspace.content.authority.ChoiceAuthority; +import org.dspace.content.authority.service.ChoiceAuthorityService; +import org.dspace.content.service.CollectionService; +import org.dspace.content.service.MetadataFieldService; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Component; + +/** + * Controller for exposition of vocabularies for the submission + * + * @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it) + * @author Andrea Bollini (andrea.bollini at 4science.it) + */ +@Component(VocabularyRest.CATEGORY + "." + VocabularyRest.NAME) +public class VocabularyRestRepository extends DSpaceRestRepository { + + @Autowired + private ChoiceAuthorityService cas; + + @Autowired + private AuthorityUtils authorityUtils; + + @Autowired + private CollectionService collectionService; + + @Autowired + private MetadataFieldService metadataFieldService; + + @PreAuthorize("hasAuthority('AUTHENTICATED')") + @Override + public VocabularyRest findOne(Context context, String name) { + ChoiceAuthority source = cas.getChoiceAuthorityByAuthorityName(name); + return authorityUtils.convertAuthority(source, name, utils.obtainProjection()); + } + + @PreAuthorize("hasAuthority('AUTHENTICATED')") + @Override + public Page findAll(Context context, Pageable pageable) { + Set authoritiesName = cas.getChoiceAuthoritiesNames(); + List results = new ArrayList<>(); + Projection projection = utils.obtainProjection(); + for (String authorityName : authoritiesName) { + ChoiceAuthority source = cas.getChoiceAuthorityByAuthorityName(authorityName); + VocabularyRest result = authorityUtils.convertAuthority(source, authorityName, projection); + results.add(result); + } + return utils.getPage(results, pageable); + } + + @PreAuthorize("hasAuthority('AUTHENTICATED')") + @SearchRestMethod(name = "byMetadataAndCollection") + public VocabularyRest findByMetadataAndCollection( + @Parameter(value = "metadata", required = true) String metadataField, + @Parameter(value = "collection", required = true) UUID collectionUuid) { + + Collection collection = null; + MetadataField metadata = null; + String[] tokens = org.dspace.core.Utils.tokenize(metadataField); + + try { + collection = collectionService.find(obtainContext(), collectionUuid); + metadata = metadataFieldService.findByElement(obtainContext(), tokens[0], tokens[1], tokens[2]); + } catch (SQLException e) { + throw new RuntimeException( + "A database error occurs retrieving the metadata and/or the collection information", e); + } + + if (metadata == null) { + throw new UnprocessableEntityException(metadataField + " is not a valid metadata"); + } + if (collection == null) { + throw new UnprocessableEntityException(collectionUuid + " is not a valid collection"); + } + + String authorityName = cas.getChoiceAuthorityName(tokens[0], tokens[1], tokens[2], collection); + ChoiceAuthority source = cas.getChoiceAuthorityByAuthorityName(authorityName); + return authorityUtils.convertAuthority(source, authorityName, utils.obtainProjection()); + } + + @Override + public Class getDomainClass() { + return VocabularyRest.class; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/AuthorityUtils.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/AuthorityUtils.java index 97be32ecf9..1a2a071fe1 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/AuthorityUtils.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/AuthorityUtils.java @@ -7,9 +7,11 @@ */ package org.dspace.app.rest.utils; +import org.apache.commons.lang3.StringUtils; import org.dspace.app.rest.converter.ConverterService; -import org.dspace.app.rest.model.AuthorityEntryRest; -import org.dspace.app.rest.model.AuthorityRest; +import org.dspace.app.rest.model.VocabularyEntryDetailsRest; +import org.dspace.app.rest.model.VocabularyEntryRest; +import org.dspace.app.rest.model.VocabularyRest; import org.dspace.app.rest.projection.Projection; import org.dspace.content.authority.Choice; import org.dspace.content.authority.ChoiceAuthority; @@ -27,6 +29,8 @@ public class AuthorityUtils { public static final String PRESENTATION_TYPE_LOOKUP = "lookup"; + public static final String PRESENTATION_TYPE_AUTHORLOOKUP = "authorLookup"; + public static final String PRESENTATION_TYPE_SUGGEST = "suggest"; public static final String RESERVED_KEYMAP_PARENT = "parent"; @@ -39,11 +43,11 @@ public class AuthorityUtils { public boolean isChoice(String schema, String element, String qualifier) { - return cas.isChoicesConfigured(org.dspace.core.Utils.standardize(schema, element, qualifier, "_")); + return cas.isChoicesConfigured(org.dspace.core.Utils.standardize(schema, element, qualifier, "_"), null); } public String getAuthorityName(String schema, String element, String qualifier) { - return cas.getChoiceAuthorityName(schema, element, qualifier); + return cas.getChoiceAuthorityName(schema, element, qualifier, null); } public boolean isClosed(String schema, String element, String qualifier) { @@ -62,9 +66,45 @@ public class AuthorityUtils { * @param projection the name of the projection to use, or {@code null}. * @return */ - public AuthorityEntryRest convertEntry(Choice choice, String authorityName, Projection projection) { - AuthorityEntryRest entry = converter.toRest(choice, projection); - entry.setAuthorityName(authorityName); + public VocabularyEntryDetailsRest convertEntryDetails(Choice choice, String authorityName, + boolean isHierarchical, Projection projection) { + if (choice == null) { + return null; + } + VocabularyEntryDetailsRest entry = converter.toRest(choice, projection); + entry.setVocabularyName(authorityName); + entry.setId(authorityName + ":" + entry.getId()); + entry.setInHierarchicalVocabulary(isHierarchical); + return entry; + } + + /** + * This utility method is currently a workaround to enrich the REST object with + * information from the parent vocabulary that is not referenced by the Choice + * model + * + * @param choice the dspace-api choice to expose as vocabulary entry + * @param authorityName the name of the vocabulary + * @param storeAuthority true if the entry id should be exposed as + * an authority for storing it in the metadatavalue + * @param projection the rest projection to apply + * @return the vocabulary entry rest reppresentation of the provided choice + */ + public VocabularyEntryRest convertEntry(Choice choice, String authorityName, boolean storeAuthority, + Projection projection) { + if (choice == null) { + return null; + } + VocabularyEntryRest entry = new VocabularyEntryRest(); + entry.setDisplay(choice.label); + entry.setValue(choice.value); + entry.setOtherInformation(choice.extras); + if (storeAuthority) { + entry.setAuthority(choice.authority); + } + if (StringUtils.isNotBlank(choice.authority)) { + entry.setVocabularyEntryDetailsRest(converter.toRest(choice, projection)); + } return entry; } @@ -76,8 +116,8 @@ public class AuthorityUtils { * @param projection the projecton to use. * @return */ - public AuthorityRest convertAuthority(ChoiceAuthority source, String authorityName, Projection projection) { - AuthorityRest result = converter.toRest(source, projection); + public VocabularyRest convertAuthority(ChoiceAuthority source, String authorityName, Projection projection) { + VocabularyRest result = converter.toRest(source, projection); result.setName(authorityName); return result; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/RegexUtils.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/RegexUtils.java index 8e887261db..ea0b793055 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/RegexUtils.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/RegexUtils.java @@ -28,7 +28,8 @@ public class RegexUtils { * identifier (digits or uuid) */ public static final String REGEX_REQUESTMAPPING_IDENTIFIER_AS_STRING_VERSION_STRONG = "/{id:^(?!^\\d+$)" + - "(?!^[0-9a-fxA-FX]{8}-[0-9a-fxA-FX]{4}-[0-9a-fxA-FX]{4}-[0-9a-fxA-FX]{4}-[0-9a-fxA-FX]{12}$)[\\w+\\-\\.]+$+}"; + "(?!^[0-9a-fxA-FX]{8}-[0-9a-fxA-FX]{4}-[0-9a-fxA-FX]{4}-[0-9a-fxA-FX]{4}-[0-9a-fxA-FX]{12}$)" + + "[\\w+\\-\\.:]+$+}"; /** * Regular expression in the request mapping to accept number as identifier diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java index 0b5b5d5919..f8158d9887 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java @@ -47,7 +47,6 @@ import org.apache.log4j.Logger; import org.dspace.app.rest.converter.ConverterService; import org.dspace.app.rest.exception.PaginationException; import org.dspace.app.rest.exception.RepositoryNotFoundException; -import org.dspace.app.rest.model.AuthorityRest; import org.dspace.app.rest.model.BaseObjectRest; import org.dspace.app.rest.model.CommunityRest; import org.dspace.app.rest.model.LinkRest; @@ -58,6 +57,7 @@ import org.dspace.app.rest.model.ResourcePolicyRest; import org.dspace.app.rest.model.RestAddressableModel; import org.dspace.app.rest.model.RestModel; import org.dspace.app.rest.model.VersionHistoryRest; +import org.dspace.app.rest.model.VocabularyRest; import org.dspace.app.rest.model.hateoas.EmbeddedPage; import org.dspace.app.rest.model.hateoas.HALResource; import org.dspace.app.rest.projection.CompositeProjection; @@ -254,7 +254,7 @@ public class Utils { return CommunityRest.NAME; } if (modelPlural.equals("authorities")) { - return AuthorityRest.NAME; + return VocabularyRest.NAME; } if (modelPlural.equals("resourcepolicies")) { return ResourcePolicyRest.NAME; @@ -268,6 +268,9 @@ public class Utils { if (StringUtils.equals(modelPlural, "properties")) { return PropertyRest.NAME; } + if (StringUtils.equals(modelPlural, "vocabularies")) { + return VocabularyRest.NAME; + } return modelPlural.replaceAll("s$", ""); } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthorityRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthorityRestRepositoryIT.java deleted file mode 100644 index 089f781902..0000000000 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthorityRestRepositoryIT.java +++ /dev/null @@ -1,233 +0,0 @@ -/** - * 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.app.rest; - -import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; -import static org.hamcrest.Matchers.is; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import java.util.Date; -import java.util.UUID; - -import org.apache.solr.client.solrj.SolrQuery; -import org.apache.solr.client.solrj.response.QueryResponse; -import org.dspace.app.rest.matcher.AuthorityEntryMatcher; -import org.dspace.app.rest.test.AbstractControllerIntegrationTest; -import org.dspace.authority.PersonAuthorityValue; -import org.dspace.authority.factory.AuthorityServiceFactory; -import org.dspace.content.authority.service.ChoiceAuthorityService; -import org.dspace.core.service.PluginService; -import org.dspace.services.ConfigurationService; -import org.hamcrest.Matchers; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.springframework.beans.factory.annotation.Autowired; - -/** - * This class handles all Authority related IT. It alters some config to run the tests, but it gets cleared again - * after every test - */ -public class AuthorityRestRepositoryIT extends AbstractControllerIntegrationTest { - - @Autowired - ConfigurationService configurationService; - - @Autowired - private PluginService pluginService; - - @Autowired - private ChoiceAuthorityService cas; - - @Before - public void setup() throws Exception { - super.setUp(); - configurationService.setProperty("plugin.named.org.dspace.content.authority.ChoiceAuthority", - "org.dspace.content.authority.SolrAuthority = SolrAuthorAuthority"); - - configurationService.setProperty("solr.authority.server", - "${solr.server}/authority"); - configurationService.setProperty("choices.plugin.dc.contributor.author", - "SolrAuthorAuthority"); - configurationService.setProperty("choices.presentation.dc.contributor.author", - "authorLookup"); - configurationService.setProperty("authority.controlled.dc.contributor.author", - "true"); - - configurationService.setProperty("authority.author.indexer.field.1", - "dc.contributor.author"); - - - // These clears have to happen so that the config is actually reloaded in those classes. This is needed for - // the properties that we're altering above and this is only used within the tests - pluginService.clearNamedPluginClasses(); - cas.clearCache(); - - PersonAuthorityValue person1 = new PersonAuthorityValue(); - person1.setId(String.valueOf(UUID.randomUUID())); - person1.setLastName("Shirasaka"); - person1.setFirstName("Seiko"); - person1.setValue("Shirasaka, Seiko"); - person1.setField("dc_contributor_author"); - person1.setLastModified(new Date()); - person1.setCreationDate(new Date()); - AuthorityServiceFactory.getInstance().getAuthorityIndexingService().indexContent(person1); - - PersonAuthorityValue person2 = new PersonAuthorityValue(); - person2.setId(String.valueOf(UUID.randomUUID())); - person2.setLastName("Miller"); - person2.setFirstName("Tyler E"); - person2.setValue("Miller, Tyler E"); - person2.setField("dc_contributor_author"); - person2.setLastModified(new Date()); - person2.setCreationDate(new Date()); - AuthorityServiceFactory.getInstance().getAuthorityIndexingService().indexContent(person2); - - AuthorityServiceFactory.getInstance().getAuthorityIndexingService().commit(); - } - - @Test - public void correctSrscQueryTest() throws Exception { - String token = getAuthToken(admin.getEmail(), password); - getClient(token).perform( - get("/api/integration/authorities/srsc/entries") - .param("metadata", "dc.subject") - .param("query", "Research") - .param("size", "1000")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.page.totalElements", Matchers.is(26))); - } - - @Test - public void noResultsSrscQueryTest() throws Exception { - String token = getAuthToken(admin.getEmail(), password); - getClient(token).perform( - get("/api/integration/authorities/srsc/entries") - .param("metadata", "dc.subject") - .param("query", "Research2") - .param("size", "1000")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.page.totalElements", Matchers.is(0))); - } - - @Test - @Ignore - /** - * This functionality is currently broken, it returns all 22 values - */ - public void correctCommonTypesTest() throws Exception { - String token = getAuthToken(admin.getEmail(), password); - getClient(token).perform( - get("/api/integration/authorities/common_types/entries") - .param("metadata", "dc.type") - .param("query", "Book") - .param("size", "1000")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.page.totalElements", Matchers.is(2))); - } - - @Test - public void correctSolrQueryTest() throws Exception { - String token = getAuthToken(admin.getEmail(), password); - getClient(token).perform( - get("/api/integration/authorities/SolrAuthorAuthority/entries") - .param("metadata", "dc.contributor.author") - .param("query", "Shirasaka") - .param("size", "1000")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.page.totalElements", Matchers.is(1))); - } - - @Test - public void noResultsSolrQueryTest() throws Exception { - String token = getAuthToken(admin.getEmail(), password); - getClient(token).perform( - get("/api/integration/authorities/SolrAuthorAuthority/entries") - .param("metadata", "dc.contributor.author") - .param("query", "Smith") - .param("size", "1000")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.page.totalElements", Matchers.is(0))); - } - - @Test - public void retrieveSrscValueTest() throws Exception { - String token = getAuthToken(admin.getEmail(), password); - - // When full projection is requested, response should include expected properties, links, and embeds. - getClient(token).perform( - get("/api/integration/authorities/srsc/entryValues/SCB1922").param("projection", "full")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$", AuthorityEntryMatcher.matchFullEmbeds())) - .andExpect(jsonPath("$.page.totalElements", Matchers.is(1))); - } - - @Test - public void noResultsSrscValueTest() throws Exception { - String token = getAuthToken(admin.getEmail(), password); - getClient(token).perform( - get("/api/integration/authorities/srsc/entryValues/DOESNTEXIST")) - .andExpect(status().isNotFound()); - } - - @Test - public void retrieveCommonTypesValueTest() throws Exception { - String token = getAuthToken(admin.getEmail(), password); - getClient(token).perform( - get("/api/integration/authorities/common_types/entryValues/Book").param("projection", "full")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.page.totalElements", Matchers.is(1))) - ; - - } - - @Test - public void retrieveCommonTypesWithSpaceValueTest() throws Exception { - String token = getAuthToken(admin.getEmail(), password); - getClient(token).perform( - get("/api/integration/authorities/common_types/entryValues/Learning+Object")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.page.totalElements", Matchers.is(1))); - } - - @Test - public void discoverableNestedLinkTest() throws Exception { - String token = getAuthToken(eperson.getEmail(), password); - getClient(token).perform(get("/api")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$._links",Matchers.allOf( - hasJsonPath("$.authorizations.href", - is("http://localhost/api/authz/authorizations")), - hasJsonPath("$.authorization-search.href", - is("http://localhost/api/authz/authorization/search")) - ))); - } - - @Test - public void retrieveSolrValueTest() throws Exception { - String token = getAuthToken(admin.getEmail(), password); - - SolrQuery query = new SolrQuery(); - query.setQuery("*:*"); - QueryResponse queryResponse = AuthorityServiceFactory.getInstance().getAuthoritySearchService().search(query); - String id = String.valueOf(queryResponse.getResults().get(0).getFieldValue("id")); - - getClient(token).perform( - get("/api/integration/authorities/SolrAuthorAuthority/entryValues/" + id)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.page.totalElements", Matchers.is(1))); - } - - @Override - public void destroy() throws Exception { - AuthorityServiceFactory.getInstance().getAuthorityIndexingService().cleanIndex(); - super.destroy(); - } -} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RootRestResourceControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RootRestResourceControllerIT.java index 6c1c4a9427..b4cee1672e 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RootRestResourceControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RootRestResourceControllerIT.java @@ -49,7 +49,8 @@ public class RootRestResourceControllerIT extends AbstractControllerIntegrationT //We expect the content type to be "application/hal+json;charset=UTF-8" .andExpect(content().contentType(contentType)) //Check that all required root links are present and that they are absolute - .andExpect(jsonPath("$._links.authorities.href", startsWith(BASE_REST_SERVER_URL))) + .andExpect(jsonPath("$._links.vocabularies.href", startsWith(BASE_REST_SERVER_URL))) + .andExpect(jsonPath("$._links.vocabularyEntryDetails.href", startsWith(BASE_REST_SERVER_URL))) .andExpect(jsonPath("$._links.bitstreamformats.href", startsWith(BASE_REST_SERVER_URL))) .andExpect(jsonPath("$._links.bitstreams.href", startsWith(BASE_REST_SERVER_URL))) .andExpect(jsonPath("$._links.browses.href", startsWith(BASE_REST_SERVER_URL))) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionFormsControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionFormsControllerIT.java index f754d969fd..66938e5991 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionFormsControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionFormsControllerIT.java @@ -23,6 +23,9 @@ import org.dspace.app.rest.repository.SubmissionFormRestRepository; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.app.util.DCInputsReaderException; import org.dspace.builder.EPersonBuilder; +import org.dspace.content.authority.DCInputAuthority; +import org.dspace.content.authority.service.ChoiceAuthorityService; +import org.dspace.core.service.PluginService; import org.dspace.eperson.EPerson; import org.dspace.services.ConfigurationService; import org.hamcrest.Matchers; @@ -39,6 +42,10 @@ public class SubmissionFormsControllerIT extends AbstractControllerIntegrationTe private ConfigurationService configurationService; @Autowired private SubmissionFormRestRepository submissionFormRestRepository; + @Autowired + private PluginService pluginService; + @Autowired + private ChoiceAuthorityService cas; @Test public void findAll() throws Exception { @@ -56,15 +63,15 @@ public class SubmissionFormsControllerIT extends AbstractControllerIntegrationTe .andExpect(status().isOk()) //We expect the content type to be "application/hal+json;charset=UTF-8" .andExpect(content().contentType(contentType)) - //The configuration file for the test env includes 3 forms + //The configuration file for the test env includes 6 forms .andExpect(jsonPath("$.page.size", is(20))) - .andExpect(jsonPath("$.page.totalElements", equalTo(4))) + .andExpect(jsonPath("$.page.totalElements", equalTo(6))) .andExpect(jsonPath("$.page.totalPages", equalTo(1))) .andExpect(jsonPath("$.page.number", is(0))) .andExpect( jsonPath("$._links.self.href", Matchers.startsWith(REST_SERVER_URL + "config/submissionforms"))) - //The array of submissionforms should have a size of 3 - .andExpect(jsonPath("$._embedded.submissionforms", hasSize(equalTo(4)))) + //The array of submissionforms should have a size of 6 + .andExpect(jsonPath("$._embedded.submissionforms", hasSize(equalTo(6)))) ; } @@ -75,12 +82,12 @@ public class SubmissionFormsControllerIT extends AbstractControllerIntegrationTe .andExpect(status().isOk()) .andExpect(content().contentType(contentType)) .andExpect(jsonPath("$.page.size", is(20))) - .andExpect(jsonPath("$.page.totalElements", equalTo(4))) + .andExpect(jsonPath("$.page.totalElements", equalTo(6))) .andExpect(jsonPath("$.page.totalPages", equalTo(1))) .andExpect(jsonPath("$.page.number", is(0))) .andExpect(jsonPath("$._links.self.href", Matchers.startsWith(REST_SERVER_URL + "config/submissionforms"))) - .andExpect(jsonPath("$._embedded.submissionforms", hasSize(equalTo(4)))); + .andExpect(jsonPath("$._embedded.submissionforms", hasSize(equalTo(6)))); } @Test @@ -152,6 +159,121 @@ public class SubmissionFormsControllerIT extends AbstractControllerIntegrationTe "col-sm-8","dc.publisher")))); } + @Test + public void findFieldWithAuthorityConfig() throws Exception { + configurationService.setProperty("plugin.named.org.dspace.content.authority.ChoiceAuthority", + new String[] { + "org.dspace.content.authority.SolrAuthority = SolrAuthorAuthority", + "org.dspace.content.authority.SolrAuthority = SolrEditorAuthority", + "org.dspace.content.authority.SolrAuthority = SolrSubjectAuthority" + }); + + configurationService.setProperty("solr.authority.server", + "${solr.server}/authority"); + configurationService.setProperty("choices.plugin.dc.contributor.author", + "SolrAuthorAuthority"); + configurationService.setProperty("choices.presentation.dc.contributor.author", + "suggest"); + configurationService.setProperty("authority.controlled.dc.contributor.author", + "true"); + configurationService.setProperty("authority.author.indexer.field.1", + "dc.contributor.author"); + configurationService.setProperty("choices.plugin.dc.contributor.editor", + "SolrEditorAuthority"); + configurationService.setProperty("choices.presentation.dc.contributor.editor", + "authorLookup"); + configurationService.setProperty("authority.controlled.dc.contributor.editor", + "true"); + configurationService.setProperty("authority.author.indexer.field.2", + "dc.contributor.editor"); + configurationService.setProperty("choices.plugin.dc.subject", + "SolrSubjectAuthority"); + configurationService.setProperty("choices.presentation.dc.subject", + "lookup"); + configurationService.setProperty("authority.controlled.dc.subject", + "true"); + configurationService.setProperty("authority.author.indexer.field.3", + "dc.subject"); + + // These clears have to happen so that the config is actually reloaded in those classes. This is needed for + // the properties that we're altering above and this is only used within the tests + submissionFormRestRepository.reload(); + DCInputAuthority.reset(); + pluginService.clearNamedPluginClasses(); + cas.clearCache(); + + String token = getAuthToken(admin.getEmail(), password); + + getClient(token).perform(get("/api/config/submissionforms/sampleauthority")) + //The status has to be 200 OK + .andExpect(status().isOk()) + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + //Check that the JSON root matches the expected "sampleauthority" input forms + .andExpect(jsonPath("$.id", is("sampleauthority"))) + .andExpect(jsonPath("$.name", is("sampleauthority"))) + .andExpect(jsonPath("$.type", is("submissionform"))) + .andExpect(jsonPath("$._links.self.href", Matchers + .startsWith(REST_SERVER_URL + "config/submissionforms/sampleauthority"))) + // our test configuration include the dc.contributor.author, dc.contributor.editor and + // dc.subject fields with in separate rows all linked to an authority with different + // presentation modes (suggestion, name-lookup, lookup) + .andExpect(jsonPath("$.rows[0].fields", contains( + SubmissionFormFieldMatcher.matchFormFieldDefinition("onebox", "Author", + null, true, + "Author field that can be associated with an authority providing suggestion", + null, "dc.contributor.author", "SolrAuthorAuthority") + ))) + .andExpect(jsonPath("$.rows[1].fields", contains( + SubmissionFormFieldMatcher.matchFormFieldDefinition("lookup-name", "Editor", + null, false, + "Editor field that can be associated with an authority " + + "providing the special name lookup", + null, "dc.contributor.editor", "SolrEditorAuthority") + ))) + .andExpect(jsonPath("$.rows[2].fields", contains( + SubmissionFormFieldMatcher.matchFormFieldDefinition("lookup", "Subject", + null, true, + "Subject field that can be associated with an authority providing lookup", + null, "dc.subject", "SolrSubjectAuthority") + ))) + ; + // we need to force a reload of the config now to be able to reload also the cache of the other + // authority related services. As this is needed just by this test method it is more efficient do it + // here instead that force these reload for each method extending the destroy method + configurationService.reloadConfig(); + submissionFormRestRepository.reload(); + DCInputAuthority.reset(); + pluginService.clearNamedPluginClasses(); + cas.clearCache(); + } + + @Test + public void findFieldWithValuePairsConfig() throws Exception { + String token = getAuthToken(admin.getEmail(), password); + + getClient(token).perform(get("/api/config/submissionforms/traditionalpageone")) + //The status has to be 200 OK + .andExpect(status().isOk()) + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + //Check that the JSON root matches the expected "traditionalpageone" input forms + .andExpect(jsonPath("$.id", is("traditionalpageone"))) + .andExpect(jsonPath("$.name", is("traditionalpageone"))) + .andExpect(jsonPath("$.type", is("submissionform"))) + .andExpect(jsonPath("$._links.self.href", Matchers + .startsWith(REST_SERVER_URL + "config/submissionforms/traditionalpageone"))) + // our test configuration include the dc.type field with a value pair in the 8th row + .andExpect(jsonPath("$.rows[7].fields", contains( + SubmissionFormFieldMatcher.matchFormFieldDefinition("dropdown", "Type", + null, true, + "Select the type(s) of content of the item. To select more than one value in the " + + "list, you may have to hold down the \"CTRL\" or \"Shift\" key.", + null, "dc.type", "common_types") + ))) + ; + } + @Test public void findOpenRelationshipConfig() throws Exception { String token = getAuthToken(admin.getEmail(), password); @@ -203,8 +325,14 @@ public class SubmissionFormsControllerIT extends AbstractControllerIntegrationTe public void languageSupportTest() throws Exception { context.turnOffAuthorisationSystem(); String[] supportedLanguage = {"it","uk"}; + configurationService.setProperty("default.locale","it"); configurationService.setProperty("webui.supported.locales",supportedLanguage); + // These clears have to happen so that the config is actually reloaded in those classes. This is needed for + // the properties that we're altering above and this is only used within the tests submissionFormRestRepository.reload(); + DCInputAuthority.reset(); + pluginService.clearNamedPluginClasses(); + cas.clearCache(); Locale uk = new Locale("uk"); Locale it = new Locale("it"); @@ -234,7 +362,8 @@ public class SubmissionFormsControllerIT extends AbstractControllerIntegrationTe "Selezionare la lingua del contenuto principale dell'item." + " Se la lingua non compare nell'elenco, selezionare (Altro)." + " Se il contenuto non ha davvero una lingua" - + " (ad esempio, se è un set di dati o un'immagine) selezionare (N/A)", "dc.language.iso")))); + + " (ad esempio, se è un set di dati o un'immagine) selezionare (N/A)", + null, "dc.language.iso", "common_iso_languages")))); // user select ukranian language getClient(tokenEperson).perform(get("/api/config/submissionforms/languagetest").locale(uk)) @@ -256,9 +385,8 @@ public class SubmissionFormsControllerIT extends AbstractControllerIntegrationTe .matchFormFieldDefinition("dropdown", "Мова", null, false, "Виберiть мову головного змiсту файлу, як що мови немає у списку, вибрати (Iнша)." + " Як що вмiст вайлу не є текстовим, наприклад є фотографiєю, тодi вибрати (N/A)", - "dc.language.iso")))); - - resetLocalesConfiguration(); + null, "dc.language.iso", "common_iso_languages")))); + resetLocalesConfiguration(); } @Test @@ -266,8 +394,14 @@ public class SubmissionFormsControllerIT extends AbstractControllerIntegrationTe context.turnOffAuthorisationSystem(); String[] supportedLanguage = {"it","uk"}; + configurationService.setProperty("default.locale","it"); configurationService.setProperty("webui.supported.locales",supportedLanguage); + // These clears have to happen so that the config is actually reloaded in those classes. This is needed for + // the properties that we're altering above and this is only used within the tests submissionFormRestRepository.reload(); + DCInputAuthority.reset(); + pluginService.clearNamedPluginClasses(); + cas.clearCache(); EPerson epersonIT = EPersonBuilder.createEPerson(context) .withEmail("epersonIT@example.com") @@ -307,7 +441,8 @@ public class SubmissionFormsControllerIT extends AbstractControllerIntegrationTe "Selezionare la lingua del contenuto principale dell'item." + " Se la lingua non compare nell'elenco, selezionare (Altro)." + " Se il contenuto non ha davvero una lingua" - + " (ad esempio, se è un set di dati o un'immagine) selezionare (N/A)", "dc.language.iso")))); + + " (ad esempio, se è un set di dati o un'immagine) selezionare (N/A)", + null, "dc.language.iso", "common_iso_languages")))); // user with ukranian prefer language getClient(tokenEpersonUK).perform(get("/api/config/submissionforms/languagetest")) @@ -329,9 +464,8 @@ public class SubmissionFormsControllerIT extends AbstractControllerIntegrationTe .matchFormFieldDefinition("dropdown", "Мова", null, false, "Виберiть мову головного змiсту файлу, як що мови немає у списку, вибрати (Iнша)." + " Як що вмiст вайлу не є текстовим, наприклад є фотографiєю, тодi вибрати (N/A)", - "dc.language.iso")))); - - resetLocalesConfiguration(); + null, "dc.language.iso", "common_iso_languages")))); + resetLocalesConfiguration(); } @Test @@ -339,8 +473,14 @@ public class SubmissionFormsControllerIT extends AbstractControllerIntegrationTe context.turnOffAuthorisationSystem(); String[] supportedLanguage = {"it","uk"}; + configurationService.setProperty("default.locale","it"); configurationService.setProperty("webui.supported.locales",supportedLanguage); + // These clears have to happen so that the config is actually reloaded in those classes. This is needed for + // the properties that we're altering above and this is only used within the tests submissionFormRestRepository.reload(); + DCInputAuthority.reset(); + pluginService.clearNamedPluginClasses(); + cas.clearCache(); Locale it = new Locale("it"); @@ -375,9 +515,9 @@ public class SubmissionFormsControllerIT extends AbstractControllerIntegrationTe "Selezionare la lingua del contenuto principale dell'item." + " Se la lingua non compare nell'elenco, selezionare (Altro)." + " Se il contenuto non ha davvero una lingua" - + " (ad esempio, se è un set di dati o un'immagine) selezionare (N/A)", "dc.language.iso")))); - - resetLocalesConfiguration(); + + " (ad esempio, se è un set di dati o un'immagine) selezionare (N/A)", + null, "dc.language.iso", "common_iso_languages")))); + resetLocalesConfiguration(); } @Test @@ -387,7 +527,12 @@ public class SubmissionFormsControllerIT extends AbstractControllerIntegrationTe String[] supportedLanguage = {"it","uk"}; configurationService.setProperty("default.locale","it"); configurationService.setProperty("webui.supported.locales",supportedLanguage); + // These clears have to happen so that the config is actually reloaded in those classes. This is needed for + // the properties that we're altering above and this is only used within the tests submissionFormRestRepository.reload(); + DCInputAuthority.reset(); + pluginService.clearNamedPluginClasses(); + cas.clearCache(); context.restoreAuthSystemState(); @@ -407,8 +552,7 @@ public class SubmissionFormsControllerIT extends AbstractControllerIntegrationTe .matchFormFieldDefinition("onebox", "Titolo", "\u00C8 necessario inserire un titolo principale per questo item", false, "Inserisci titolo principale di questo item", "dc.title")))); - - resetLocalesConfiguration(); + resetLocalesConfiguration(); } @Test @@ -417,7 +561,12 @@ public class SubmissionFormsControllerIT extends AbstractControllerIntegrationTe String[] supportedLanguage = {"it","uk","en"}; configurationService.setProperty("default.locale","en"); configurationService.setProperty("webui.supported.locales",supportedLanguage); + // These clears have to happen so that the config is actually reloaded in those classes. This is needed for + // the properties that we're altering above and this is only used within the tests submissionFormRestRepository.reload(); + DCInputAuthority.reset(); + pluginService.clearNamedPluginClasses(); + cas.clearCache(); context.restoreAuthSystemState(); @@ -446,5 +595,8 @@ public class SubmissionFormsControllerIT extends AbstractControllerIntegrationTe configurationService.setProperty("default.locale","en"); configurationService.setProperty("webui.supported.locales",null); submissionFormRestRepository.reload(); + DCInputAuthority.reset(); + pluginService.clearNamedPluginClasses(); + cas.clearCache(); } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/VocabularyEntryDetailsIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/VocabularyEntryDetailsIT.java new file mode 100644 index 0000000000..e5eab1aa98 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/VocabularyEntryDetailsIT.java @@ -0,0 +1,442 @@ +/** + * 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.app.rest; + +import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.UUID; + +import org.dspace.app.rest.matcher.VocabularyEntryDetailsMatcher; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; +import org.junit.Test; + +/** + * + * + * @author Mykhaylo Boychuk (4science.it) + */ +public class VocabularyEntryDetailsIT extends AbstractControllerIntegrationTest { + @Test + public void discoverableNestedLinkTest() throws Exception { + String token = getAuthToken(eperson.getEmail(), password); + getClient(token).perform(get("/api")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._links",Matchers.allOf( + hasJsonPath("$.vocabularyEntryDetails.href", + is("http://localhost/api/submission/vocabularyEntryDetails")), + hasJsonPath("$.vocabularyEntryDetails-search.href", + is("http://localhost/api/submission/vocabularyEntryDetails/search")) + ))); + } + + @Test + public void findAllTest() throws Exception { + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken).perform(get("/api/submission/vocabularyEntryDetails")) + .andExpect(status() + .isMethodNotAllowed()); + } + + @Test + public void findOneTest() throws Exception { + String idAuthority = "srsc:SCB110"; + String token = getAuthToken(eperson.getEmail(), password); + getClient(token).perform(get("/api/submission/vocabularyEntryDetails/" + idAuthority)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB110", "Religion/Theology", + "Research Subject Categories::HUMANITIES and RELIGION::Religion/Theology"))) + .andExpect(jsonPath("$.selectable", is(true))) + .andExpect(jsonPath("$.otherInformation.id", is("SCB110"))) + .andExpect(jsonPath("$.otherInformation.note", is("Religionsvetenskap/Teologi"))) + .andExpect(jsonPath("$.otherInformation.parent", + is("Research Subject Categories::HUMANITIES and RELIGION"))) + .andExpect(jsonPath("$._links.parent.href", + endsWith("api/submission/vocabularyEntryDetails/srsc:SCB110/parent"))) + .andExpect(jsonPath("$._links.children.href", + endsWith("api/submission/vocabularyEntryDetails/srsc:SCB110/children"))); + } + + @Test + public void findOneUnauthorizedTest() throws Exception { + String idAuthority = "srsc:SCB110"; + getClient().perform(get("/api/submission/vocabularyEntryDetails/" + idAuthority)) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findOneNotFoundTest() throws Exception { + String idAuthority = "srsc:not-existing"; + String token = getAuthToken(eperson.getEmail(), password); + getClient(token).perform(get("/api/submission/vocabularyEntryDetails/" + idAuthority)) + .andExpect(status().isNotFound()); + + // try with a special id missing only the entry-id part + getClient(token).perform(get("/api/submission/vocabularyEntryDetails/srsc:")) + .andExpect(status().isNotFound()); + + // try to retrieve the xml root that is not a entry itself + getClient(token).perform(get("/api/submission/vocabularyEntryDetails/srsc:ResearchSubjectCategories")) + .andExpect(status().isNotFound()); + + } + + @Test + public void srscSearchTopTest() throws Exception { + String tokenAdmin = getAuthToken(admin.getEmail(), password); + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + getClient(tokenAdmin).perform(get("/api/submission/vocabularyEntryDetails/search/top") + .param("vocabulary", "srsc")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.vocabularyEntryDetails", Matchers.containsInAnyOrder( + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB11", "HUMANITIES and RELIGION", + "Research Subject Categories::HUMANITIES and RELIGION"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB12", "LAW/JURISPRUDENCE", + "Research Subject Categories::LAW/JURISPRUDENCE"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB13", "SOCIAL SCIENCES", + "Research Subject Categories::SOCIAL SCIENCES"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB14", "MATHEMATICS", + "Research Subject Categories::MATHEMATICS"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB15", "NATURAL SCIENCES", + "Research Subject Categories::NATURAL SCIENCES"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB16", "TECHNOLOGY", + "Research Subject Categories::TECHNOLOGY"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB17", + "FORESTRY, AGRICULTURAL SCIENCES and LANDSCAPE PLANNING", + "Research Subject Categories::FORESTRY, AGRICULTURAL SCIENCES and LANDSCAPE PLANNING"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB18", "MEDICINE", + "Research Subject Categories::MEDICINE"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB19", "ODONTOLOGY", + "Research Subject Categories::ODONTOLOGY"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB21", "PHARMACY", + "Research Subject Categories::PHARMACY"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB22", "VETERINARY MEDICINE", + "Research Subject Categories::VETERINARY MEDICINE"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB23", "INTERDISCIPLINARY RESEARCH AREAS", + "Research Subject Categories::INTERDISCIPLINARY RESEARCH AREAS") + ))) + .andExpect(jsonPath("$.page.totalElements", Matchers.is(12))); + + getClient(tokenEPerson).perform(get("/api/submission/vocabularyEntryDetails/search/top") + .param("vocabulary", "srsc")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.vocabularyEntryDetails", Matchers.containsInAnyOrder( + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB11", "HUMANITIES and RELIGION", + "Research Subject Categories::HUMANITIES and RELIGION"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB12", "LAW/JURISPRUDENCE", + "Research Subject Categories::LAW/JURISPRUDENCE"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB13", "SOCIAL SCIENCES", + "Research Subject Categories::SOCIAL SCIENCES"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB14", "MATHEMATICS", + "Research Subject Categories::MATHEMATICS"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB15", "NATURAL SCIENCES", + "Research Subject Categories::NATURAL SCIENCES"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB16", "TECHNOLOGY", + "Research Subject Categories::TECHNOLOGY"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB17", + "FORESTRY, AGRICULTURAL SCIENCES and LANDSCAPE PLANNING", + "Research Subject Categories::FORESTRY, AGRICULTURAL SCIENCES and LANDSCAPE PLANNING"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB18", "MEDICINE", + "Research Subject Categories::MEDICINE"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB19", "ODONTOLOGY", + "Research Subject Categories::ODONTOLOGY"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB21", "PHARMACY", + "Research Subject Categories::PHARMACY"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB22", "VETERINARY MEDICINE", + "Research Subject Categories::VETERINARY MEDICINE"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB23", "INTERDISCIPLINARY RESEARCH AREAS", + "Research Subject Categories::INTERDISCIPLINARY RESEARCH AREAS") + ))) + .andExpect(jsonPath("$.page.totalElements", Matchers.is(12))); + } + + @Test + public void srscSearchFirstLevel_MATHEMATICS_Test() throws Exception { + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(get("/api/submission/vocabularyEntryDetails/srsc:SCB14/children")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.children", Matchers.containsInAnyOrder( + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB1401", + "Algebra, geometry and mathematical analysis", + "Research Subject Categories::MATHEMATICS::Algebra, geometry and mathematical analysis"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB1402", "Applied mathematics", + "Research Subject Categories::MATHEMATICS::Applied mathematics"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB1409", "Other mathematics", + "Research Subject Categories::MATHEMATICS::Other mathematics") + ))) + .andExpect(jsonPath("$._embedded.children[*].otherInformation.parent", + Matchers.everyItem(is("Research Subject Categories::MATHEMATICS")))) + .andExpect(jsonPath("$.page.totalElements", Matchers.is(3))); + } + + @Test + public void srscSearchTopPaginationTest() throws Exception { + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(get("/api/submission/vocabularyEntryDetails/search/top") + .param("vocabulary", "srsc") + .param("page", "0") + .param("size", "5")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.vocabularyEntryDetails", Matchers.containsInAnyOrder( + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB11", "HUMANITIES and RELIGION", + "Research Subject Categories::HUMANITIES and RELIGION"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB12", "LAW/JURISPRUDENCE", + "Research Subject Categories::LAW/JURISPRUDENCE"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB13", "SOCIAL SCIENCES", + "Research Subject Categories::SOCIAL SCIENCES"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB14", "MATHEMATICS", + "Research Subject Categories::MATHEMATICS"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB15", "NATURAL SCIENCES", + "Research Subject Categories::NATURAL SCIENCES") + ))) + .andExpect(jsonPath("$.page.totalElements", is(12))) + .andExpect(jsonPath("$.page.totalPages", is(3))) + .andExpect(jsonPath("$.page.number", is(0))); + + //second page + getClient(tokenAdmin).perform(get("/api/submission/vocabularyEntryDetails/search/top") + .param("vocabulary", "srsc") + .param("page", "1") + .param("size", "5")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.vocabularyEntryDetails", Matchers.containsInAnyOrder( + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB16", "TECHNOLOGY", + "Research Subject Categories::TECHNOLOGY"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB17", + "FORESTRY, AGRICULTURAL SCIENCES and LANDSCAPE PLANNING", + "Research Subject Categories::FORESTRY, AGRICULTURAL SCIENCES and LANDSCAPE PLANNING"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB18", "MEDICINE", + "Research Subject Categories::MEDICINE"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB19", "ODONTOLOGY", + "Research Subject Categories::ODONTOLOGY"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB21", "PHARMACY", + "Research Subject Categories::PHARMACY") + ))) + .andExpect(jsonPath("$.page.totalElements", is(12))) + .andExpect(jsonPath("$.page.totalPages", is(3))) + .andExpect(jsonPath("$.page.number", is(1))); + + // third page + getClient(tokenAdmin).perform(get("/api/submission/vocabularyEntryDetails/search/top") + .param("vocabulary", "srsc") + .param("page", "2") + .param("size", "5")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.vocabularyEntryDetails", Matchers.containsInAnyOrder( + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB22", "VETERINARY MEDICINE", + "Research Subject Categories::VETERINARY MEDICINE"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB23", "INTERDISCIPLINARY RESEARCH AREAS", + "Research Subject Categories::INTERDISCIPLINARY RESEARCH AREAS") + ))) + .andExpect(jsonPath("$.page.totalElements", is(12))) + .andExpect(jsonPath("$.page.totalPages", is(3))) + .andExpect(jsonPath("$.page.number", is(2))); + } + + @Test + public void searchTopBadRequestTest() throws Exception { + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(get("/api/submission/vocabularyEntryDetails/search/top") + .param("vocabulary", UUID.randomUUID().toString())) + .andExpect(status().isBadRequest()); + } + + @Test + public void searchTopUnauthorizedTest() throws Exception { + getClient().perform(get("/api/submission/vocabularyEntryDetails/search/top") + .param("vocabulary", "srsc:SCB16")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void srscSearchByParentFirstLevelPaginationTest() throws Exception { + String token = getAuthToken(eperson.getEmail(), password); + // first page + getClient(token).perform(get("/api/submission/vocabularyEntryDetails/srsc:SCB14/children") + .param("page", "0") + .param("size", "2")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.children", Matchers.containsInAnyOrder( + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB1401", + "Algebra, geometry and mathematical analysis", + "Research Subject Categories::MATHEMATICS::Algebra, geometry and mathematical analysis"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB1402", "Applied mathematics", + "Research Subject Categories::MATHEMATICS::Applied mathematics") + ))) + .andExpect(jsonPath("$.page.totalElements", is(3))) + .andExpect(jsonPath("$.page.totalPages", is(2))) + .andExpect(jsonPath("$.page.number", is(0))); + + // second page + getClient(token).perform(get("/api/submission/vocabularyEntryDetails/srsc:SCB14/children") + .param("page", "1") + .param("size", "2")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.children", Matchers.contains( + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:SCB1409", "Other mathematics", + "Research Subject Categories::MATHEMATICS::Other mathematics") + ))) + .andExpect(jsonPath("$.page.totalElements", is(3))) + .andExpect(jsonPath("$.page.totalPages", is(2))) + .andExpect(jsonPath("$.page.number", is(1))); + } + + @Test + public void srscSearchByParentSecondLevel_Applied_mathematics_Test() throws Exception { + String token = getAuthToken(eperson.getEmail(), password); + getClient(token).perform(get("/api/submission/vocabularyEntryDetails/srsc:SCB1402/children")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.children", Matchers.containsInAnyOrder( + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:VR140202", "Numerical analysis", + "Research Subject Categories::MATHEMATICS::Applied mathematics::Numerical analysis"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:VR140203", "Mathematical statistics", + "Research Subject Categories::MATHEMATICS::Applied mathematics::Mathematical statistics"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:VR140204", "Optimization, systems theory", + "Research Subject Categories::MATHEMATICS::Applied mathematics::Optimization, systems theory"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:VR140205", "Theoretical computer science", + "Research Subject Categories::MATHEMATICS::Applied mathematics::Theoretical computer science") + ))) + .andExpect(jsonPath("$.page.totalElements", Matchers.is(4))); + } + + @Test + public void srscSearchByParentEmptyTest() throws Exception { + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(get("/api/submission/vocabularyEntryDetails/srsc:VR140202/children")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", Matchers.is(0))); + } + + @Test + public void srscSearchByParentWrongIdTest() throws Exception { + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(get("/api/submission/vocabularyEntryDetails/" + + UUID.randomUUID() + "/children")) + .andExpect(status().isBadRequest()); + } + + @Test + public void srscSearchTopUnauthorizedTest() throws Exception { + getClient().perform(get("/api/submission/vocabularyEntryDetails/search/top") + .param("vocabulary", "srsc")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findParentByChildTest() throws Exception { + String tokenEperson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEperson).perform(get("/api/submission/vocabularyEntryDetails/srsc:SCB180/parent")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", is( + VocabularyEntryDetailsMatcher.matchAuthorityEntry( + "srsc:SCB18", "MEDICINE","Research Subject Categories::MEDICINE") + ))); + } + + @Test + public void findParentByChildBadRequestTest() throws Exception { + String tokenEperson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEperson).perform(get("/api/submission/vocabularyEntryDetails/" + UUID.randomUUID() + "/parent")) + .andExpect(status().isBadRequest()); + } + + @Test + public void findParentByChildUnauthorizedTest() throws Exception { + getClient().perform(get("/api/submission/vocabularyEntryDetails/srsc:SCB180/parent")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findParentTopTest() throws Exception { + String tokenEperson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEperson) + .perform(get("/api/submission/vocabularyEntryDetails/srsc:SCB11/parent")) + .andExpect(status().isNoContent()); + } + + @Test + public void srscProjectionTest() throws Exception { + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform( + get("/api/submission/vocabularyEntryDetails/srsc:SCB110").param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.parent", + VocabularyEntryDetailsMatcher.matchAuthorityEntry( + "srsc:SCB11", "HUMANITIES and RELIGION", + "Research Subject Categories::HUMANITIES and RELIGION"))) + .andExpect(jsonPath("$._embedded.children._embedded.children", matchAllSrscSC110Children())) + .andExpect(jsonPath("$._embedded.children._embedded.children[*].otherInformation.parent", + Matchers.everyItem( + is("Research Subject Categories::HUMANITIES and RELIGION::Religion/Theology")))); + + getClient(tokenAdmin).perform( + get("/api/submission/vocabularyEntryDetails/srsc:SCB110").param("embed", "children")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.children._embedded.children", matchAllSrscSC110Children())) + .andExpect(jsonPath("$._embedded.children._embedded.children[*].otherInformation.parent", + Matchers.everyItem( + is("Research Subject Categories::HUMANITIES and RELIGION::Religion/Theology")))); + + getClient(tokenAdmin).perform( + get("/api/submission/vocabularyEntryDetails/srsc:SCB110").param("embed", "parent")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.parent", + VocabularyEntryDetailsMatcher.matchAuthorityEntry( + "srsc:SCB11", "HUMANITIES and RELIGION", + "Research Subject Categories::HUMANITIES and RELIGION"))); + } + + private Matcher> matchAllSrscSC110Children() { + return Matchers.containsInAnyOrder( + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:VR110102", + "History of religion", + "Research Subject Categories::HUMANITIES and RELIGION::Religion/Theology::History of religion"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:VR110103", + "Church studies", + "Research Subject Categories::HUMANITIES and RELIGION::Religion/Theology::Church studies"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:VR110104", + "Missionary studies", + "Research Subject Categories::HUMANITIES and RELIGION::Religion/Theology::Missionary studies"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:VR110105", + "Systematic theology", + "Research Subject Categories::HUMANITIES and RELIGION::Religion/Theology::Systematic theology"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:VR110106", + "Islamology", + "Research Subject Categories::HUMANITIES and RELIGION::Religion/Theology::Islamology"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:VR110107", + "Faith and reason", + "Research Subject Categories::HUMANITIES and RELIGION::Religion/Theology::Faith and reason"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:VR110108", + "Sociology of religion", + "Research Subject Categories::HUMANITIES and RELIGION::Religion/Theology::Sociology of religion"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:VR110109", + "Psychology of religion", + "Research Subject Categories::HUMANITIES and RELIGION::Religion/Theology::Psychology of religion"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:VR110110", + "Philosophy of religion", + "Research Subject Categories::HUMANITIES and RELIGION::Religion/Theology::Philosophy of religion"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:VR110111", + "New Testament exegesis", + "Research Subject Categories::HUMANITIES and RELIGION::Religion/Theology::New Testament exegesis"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:VR110112", + "Old Testament exegesis", + "Research Subject Categories::HUMANITIES and RELIGION::Religion/Theology::Old Testament exegesis"), + VocabularyEntryDetailsMatcher.matchAuthorityEntry("srsc:VR110113", + "Dogmatics with symbolics", + "Research Subject Categories::HUMANITIES and RELIGION::Religion/Theology::Dogmatics with symbolics") + ); + } + +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/VocabularyRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/VocabularyRestRepositoryIT.java new file mode 100644 index 0000000000..738a334b82 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/VocabularyRestRepositoryIT.java @@ -0,0 +1,394 @@ +/** + * 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.app.rest; + +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Date; +import java.util.Locale; +import java.util.UUID; + +import org.dspace.app.rest.matcher.VocabularyMatcher; +import org.dspace.app.rest.repository.SubmissionFormRestRepository; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.authority.PersonAuthorityValue; +import org.dspace.authority.factory.AuthorityServiceFactory; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.content.Collection; +import org.dspace.content.authority.DCInputAuthority; +import org.dspace.content.authority.service.ChoiceAuthorityService; +import org.dspace.core.service.PluginService; +import org.dspace.services.ConfigurationService; +import org.hamcrest.Matchers; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * This class handles all Authority related IT. It alters some config to run the tests, but it gets cleared again + * after every test + */ +public class VocabularyRestRepositoryIT extends AbstractControllerIntegrationTest { + + @Autowired + ConfigurationService configurationService; + + @Autowired + private SubmissionFormRestRepository submissionFormRestRepository; + + @Autowired + private PluginService pluginService; + + @Autowired + private ChoiceAuthorityService cas; + + @Before + public void setup() throws Exception { + super.setUp(); + configurationService.setProperty("plugin.named.org.dspace.content.authority.ChoiceAuthority", + "org.dspace.content.authority.SolrAuthority = SolrAuthorAuthority"); + + configurationService.setProperty("solr.authority.server", + "${solr.server}/authority"); + configurationService.setProperty("choices.plugin.dc.contributor.author", + "SolrAuthorAuthority"); + configurationService.setProperty("choices.presentation.dc.contributor.author", + "authorLookup"); + configurationService.setProperty("authority.controlled.dc.contributor.author", + "true"); + + configurationService.setProperty("authority.author.indexer.field.1", + "dc.contributor.author"); + + // These clears have to happen so that the config is actually reloaded in those classes. This is needed for + // the properties that we're altering above and this is only used within the tests + DCInputAuthority.reset(); + pluginService.clearNamedPluginClasses(); + cas.clearCache(); + + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("A parent community for all our test") + .build(); + context.restoreAuthSystemState(); + PersonAuthorityValue person1 = new PersonAuthorityValue(); + person1.setId(String.valueOf(UUID.randomUUID())); + person1.setLastName("Shirasaka"); + person1.setFirstName("Seiko"); + person1.setValue("Shirasaka, Seiko"); + person1.setField("dc_contributor_author"); + person1.setLastModified(new Date()); + person1.setCreationDate(new Date()); + AuthorityServiceFactory.getInstance().getAuthorityIndexingService().indexContent(person1); + + PersonAuthorityValue person2 = new PersonAuthorityValue(); + person2.setId(String.valueOf(UUID.randomUUID())); + person2.setLastName("Miller"); + person2.setFirstName("Tyler E"); + person2.setValue("Miller, Tyler E"); + person2.setField("dc_contributor_author"); + person2.setLastModified(new Date()); + person2.setCreationDate(new Date()); + AuthorityServiceFactory.getInstance().getAuthorityIndexingService().indexContent(person2); + + AuthorityServiceFactory.getInstance().getAuthorityIndexingService().commit(); + } + + @Override + @After + // We need to cleanup the authorities cache once than the configuration has been restored + public void destroy() throws Exception { + super.destroy(); + DCInputAuthority.reset(); + pluginService.clearNamedPluginClasses(); + cas.clearCache(); + } + + @Test + public void findAllTest() throws Exception { + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(get("/api/submission/vocabularies")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.vocabularies", Matchers.containsInAnyOrder( + VocabularyMatcher.matchProperties("srsc", "srsc", false, true), + VocabularyMatcher.matchProperties("common_types", "common_types", true, false), + VocabularyMatcher.matchProperties("common_iso_languages", "common_iso_languages", true , false), + VocabularyMatcher.matchProperties("SolrAuthorAuthority", "SolrAuthorAuthority", false , false) + ))) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("api/submission/vocabularies"))) + .andExpect(jsonPath("$.page.totalElements", is(4))); + } + + @Test + public void findOneSRSC_Test() throws Exception { + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(get("/api/submission/vocabularies/srsc")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", is( + VocabularyMatcher.matchProperties("srsc", "srsc", false, true) + ))); + } + + @Test + public void findOneCommonTypesTest() throws Exception { + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(get("/api/submission/vocabularies/common_types")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", is( + VocabularyMatcher.matchProperties("common_types", "common_types", true, false) + ))); + } + + @Test + public void correctSrscQueryTest() throws Exception { + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform( + get("/api/submission/vocabularies/srsc/entries") + .param("filter", "Research") + .param("size", "2")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.entries", Matchers.containsInAnyOrder( + VocabularyMatcher.matchVocabularyEntry("Research Subject Categories", + "Research Subject Categories", "vocabularyEntry"), + VocabularyMatcher.matchVocabularyEntry("Family research", + "Research Subject Categories::SOCIAL SCIENCES::Social sciences::Social work::Family research", + "vocabularyEntry")))) + .andExpect(jsonPath("$.page.totalElements", Matchers.is(26))) + .andExpect(jsonPath("$.page.totalPages", Matchers.is(13))) + .andExpect(jsonPath("$.page.size", Matchers.is(2))); + } + + @Test + public void notScrollableVocabularyRequiredQueryTest() throws Exception { + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(get("/api/submission/vocabularies/srsc/entries")) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void noResultsSrscQueryTest() throws Exception { + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform( + get("/api/submission/vocabularies/srsc/entries") + .param("metadata", "dc.subject") + .param("filter", "Research2") + .param("size", "1000")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", Matchers.is(0))); + } + + @Test + public void vocabularyEntriesCommonTypesWithPaginationTest() throws Exception { + String token = getAuthToken(admin.getEmail(), password); + getClient(token) + .perform(get("/api/submission/vocabularies/common_types/entries").param("size", "2")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.entries", Matchers.containsInAnyOrder( + VocabularyMatcher.matchVocabularyEntry("Animation", "Animation", "vocabularyEntry"), + VocabularyMatcher.matchVocabularyEntry("Article", "Article", "vocabularyEntry") + ))) + .andExpect(jsonPath("$._embedded.entries[*].authority").doesNotExist()) + .andExpect(jsonPath("$.page.totalElements", Matchers.is(22))) + .andExpect(jsonPath("$.page.totalPages", Matchers.is(11))) + .andExpect(jsonPath("$.page.size", Matchers.is(2))); + + //second page + getClient(token).perform(get("/api/submission/vocabularies/common_types/entries") + .param("size", "2") + .param("page", "1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.entries", Matchers.containsInAnyOrder( + VocabularyMatcher.matchVocabularyEntry("Book", "Book", "vocabularyEntry"), + VocabularyMatcher.matchVocabularyEntry("Book chapter", "Book chapter", "vocabularyEntry") + ))) + .andExpect(jsonPath("$._embedded.entries[*].authority").doesNotExist()) + .andExpect(jsonPath("$.page.totalElements", Matchers.is(22))) + .andExpect(jsonPath("$.page.totalPages", Matchers.is(11))) + .andExpect(jsonPath("$.page.size", Matchers.is(2))); + } + + @Test + public void vocabularyEntriesCommon_typesWithQueryTest() throws Exception { + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(get("/api/submission/vocabularies/common_types/entries") + .param("filter", "Book") + .param("size", "2")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.entries", Matchers.containsInAnyOrder( + VocabularyMatcher.matchVocabularyEntry("Book", "Book", "vocabularyEntry"), + VocabularyMatcher.matchVocabularyEntry("Book chapter", "Book chapter", "vocabularyEntry") + ))) + .andExpect(jsonPath("$.page.totalElements", Matchers.is(2))) + .andExpect(jsonPath("$.page.totalPages", Matchers.is(1))) + .andExpect(jsonPath("$.page.size", Matchers.is(2))); + } + + @Test + public void vocabularyEntriesDCInputAuthorityLocalesTest() throws Exception { + String[] supportedLanguage = {"it","uk"}; + configurationService.setProperty("default.locale","it"); + configurationService.setProperty("webui.supported.locales",supportedLanguage); + // These clears have to happen so that the config is actually reloaded in those classes. This is needed for + // the properties that we're altering above and this is only used within the tests + submissionFormRestRepository.reload(); + DCInputAuthority.reset(); + pluginService.clearNamedPluginClasses(); + cas.clearCache(); + + Locale uk = new Locale("uk"); + Locale it = new Locale("it"); + + String token = getAuthToken(admin.getEmail(), password); + getClient(token) + .perform(get("/api/submission/vocabularies/common_iso_languages/entries") + .param("size", "2") + .locale(it)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.entries", Matchers.containsInAnyOrder( + VocabularyMatcher.matchVocabularyEntry("N/A", "", "vocabularyEntry"), + VocabularyMatcher.matchVocabularyEntry("Inglese (USA)", "en_US", "vocabularyEntry") + ))) + .andExpect(jsonPath("$._embedded.entries[*].authority").doesNotExist()) + .andExpect(jsonPath("$.page.totalElements", Matchers.is(12))) + .andExpect(jsonPath("$.page.totalPages", Matchers.is(6))) + .andExpect(jsonPath("$.page.size", Matchers.is(2))); + + getClient(token) + .perform(get("/api/submission/vocabularies/common_iso_languages/entries") + .param("size", "2") + .locale(uk)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.entries", Matchers.containsInAnyOrder( + VocabularyMatcher.matchVocabularyEntry("N/A", "", "vocabularyEntry"), + VocabularyMatcher.matchVocabularyEntry("Американська (USA)", "en_US", "vocabularyEntry") + ))) + .andExpect(jsonPath("$._embedded.entries[*].authority").doesNotExist()) + .andExpect(jsonPath("$.page.totalElements", Matchers.is(12))) + .andExpect(jsonPath("$.page.totalPages", Matchers.is(6))) + .andExpect(jsonPath("$.page.size", Matchers.is(2))); + + configurationService.setProperty("default.locale","en"); + configurationService.setProperty("webui.supported.locales",null); + submissionFormRestRepository.reload(); + DCInputAuthority.reset(); + pluginService.clearNamedPluginClasses(); + cas.clearCache(); + } + + + @Test + public void correctSolrQueryTest() throws Exception { + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform( + get("/api/submission/vocabularies/SolrAuthorAuthority/entries") + .param("filter", "Shirasaka") + .param("size", "1000")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.entries", Matchers.contains( + VocabularyMatcher.matchVocabularyEntry("Shirasaka, Seiko", "Shirasaka, Seiko", "vocabularyEntry") + ))) + .andExpect(jsonPath("$._embedded.entries[0].authority").isNotEmpty()) + .andExpect(jsonPath("$.page.totalElements", Matchers.is(1))); + } + + @Test + public void noResultsSolrQueryTest() throws Exception { + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform( + get("/api/submission/vocabularies/SolrAuthorAuthority/entries") + .param("filter", "Smith") + .param("size", "1000")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", Matchers.is(0))); + } + + @Test + public void findByMetadataAndCollectionTest() throws Exception { + context.turnOffAuthorisationSystem(); + Collection collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Test collection") + .build(); + context.restoreAuthSystemState(); + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(get("/api/submission/vocabularies/search/byMetadataAndCollection") + .param("metadata", "dc.type") + .param("collection", collection.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", is( + VocabularyMatcher.matchProperties("common_types", "common_types", true, false) + ))); + } + + @Test + public void findByMetadataAndCollectionUnprocessableEntityTest() throws Exception { + context.turnOffAuthorisationSystem(); + Collection collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Test collection") + .build(); + context.restoreAuthSystemState(); + + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(get("/api/submission/vocabularies/search/byMetadataAndCollection") + .param("metadata", "dc.not.exist") + .param("collection", collection.getID().toString())) + .andExpect(status().isUnprocessableEntity()); + + getClient(token).perform(get("/api/submission/vocabularies/search/byMetadataAndCollection") + .param("metadata", "dc.type") + .param("collection", UUID.randomUUID().toString())) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void findByMetadataAndCollectionBadRequestTest() throws Exception { + context.turnOffAuthorisationSystem(); + + Collection collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Test collection") + .build(); + context.restoreAuthSystemState(); + + String token = getAuthToken(admin.getEmail(), password); + //missing metadata + getClient(token).perform(get("/api/submission/vocabularies/search/byMetadataAndCollection") + .param("collection", collection.getID().toString())) + .andExpect(status().isBadRequest()); + + //missing collection + getClient(token).perform(get("/api/submission/vocabularies/search/byMetadataAndCollection") + .param("metadata", "dc.type")) + .andExpect(status().isBadRequest()); + } + + @Test + public void linkedEntitiesWithExactParamTest() throws Exception { + String token = getAuthToken(eperson.getEmail(), password); + getClient(token).perform(get("/api/submission/vocabularies/common_types/entries") + .param("filter", "Animation") + .param("exact", "true")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.entries", Matchers.contains( + VocabularyMatcher.matchVocabularyEntry("Animation", "Animation", "vocabularyEntry") + ))) + .andExpect(jsonPath("$.page.totalElements", Matchers.is(1))); + } + + @Test + public void linkedEntitiesWithFilterAndEntryIdTest() throws Exception { + String token = getAuthToken(eperson.getEmail(), password); + getClient(token).perform(get("/api/submission/vocabularies/srsc/entries") + .param("filter", "Research") + .param("entryID", "VR131402")) + .andExpect(status().isBadRequest()); + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SubmissionFormFieldMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SubmissionFormFieldMatcher.java index 67f2494cf3..773a751b9f 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SubmissionFormFieldMatcher.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SubmissionFormFieldMatcher.java @@ -28,8 +28,8 @@ public class SubmissionFormFieldMatcher { /** * Shortcut for the - * {@link SubmissionFormFieldMatcher#matchFormFieldDefinition(String, String, String, boolean, String, String, String)} - * with a null style + * {@link SubmissionFormFieldMatcher#matchFormFieldDefinition(String, String, String, boolean, String, String, String, String)} + * with a null style and vocabulary name * * @param type * the expected input type @@ -53,7 +53,9 @@ public class SubmissionFormFieldMatcher { } /** - * Check the json representation of a submission form + * Shortcut for the + * {@link SubmissionFormFieldMatcher#matchFormFieldDefinition(String, String, String, boolean, String, String, String, String)} + * with a null controlled vocabulary * * @param type * the expected input type @@ -74,13 +76,45 @@ public class SubmissionFormFieldMatcher { * @return a Matcher for all the condition above */ public static Matcher matchFormFieldDefinition(String type, String label, String mandatoryMessage, - boolean repeatable, - String hints, String style, String metadata) { + boolean repeatable, + String hints, String style, String metadata) { + return matchFormFieldDefinition(type, label, mandatoryMessage, repeatable, hints, style, metadata, null); + } + + /** + * Check the json representation of a submission form + * + * @param type + * the expected input type + * @param label + * the expected label + * @param mandatoryMessage + * the expected mandatoryMessage, can be null. If not empty the field is expected to be flagged as + * mandatory + * @param repeatable + * the expected repeatable flag + * @param hints + * the expected hints message + * @param style + * the expected style for the field, can be null. If null the corresponding json path is expected to be + * missing + * @param metadata + * the expected metadata + * @param controlled vocabulary + * the expected controlled vocabulary, can be null. If null the corresponding json path is expected to be + * missing + * @return a Matcher for all the condition above + */ + public static Matcher matchFormFieldDefinition(String type, String label, String mandatoryMessage, + boolean repeatable, String hints, String style, + String metadata, String controlledVocabulary) { return allOf( // check each field definition hasJsonPath("$.input.type", is(type)), hasJsonPath("$.label", containsString(label)), hasJsonPath("$.selectableMetadata[0].metadata", is(metadata)), + controlledVocabulary != null ? hasJsonPath("$.selectableMetadata[0].controlledVocabulary", + is(controlledVocabulary)) : hasNoJsonPath("$.selectableMetadata[0].controlledVocabulary"), mandatoryMessage != null ? hasJsonPath("$.mandatoryMessage", containsString(mandatoryMessage)) : hasNoJsonPath("$.mandatoryMessage"), hasJsonPath("$.mandatory", is(mandatoryMessage != null)), diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/AuthorityEntryMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/VocabularyEntryDetailsMatcher.java similarity index 68% rename from dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/AuthorityEntryMatcher.java rename to dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/VocabularyEntryDetailsMatcher.java index 5758d3ee65..8eb2cba3c4 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/AuthorityEntryMatcher.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/VocabularyEntryDetailsMatcher.java @@ -8,7 +8,6 @@ package org.dspace.app.rest.matcher; import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; -import static org.dspace.app.rest.matcher.HalMatcher.matchEmbeds; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; @@ -18,20 +17,20 @@ import org.hamcrest.Matcher; /** * This matcher has been created so that we can use a predefined Matcher class to verify Authority Entries */ -public class AuthorityEntryMatcher { +public class VocabularyEntryDetailsMatcher { - private AuthorityEntryMatcher() { + private VocabularyEntryDetailsMatcher() { } public static Matcher matchAuthorityEntry(String id, String display, String value) { return allOf( matchProperties(id, display, value), - matchLinks()); + matchLinks(id)); } - public static Matcher matchLinks() { + public static Matcher matchLinks(String id) { return allOf( - hasJsonPath("$._links.self.href", containsString("api/integration/authority/"))); + hasJsonPath("$._links.self.href", containsString("api/submission/vocabularyEntryDetails/" + id))); } private static Matcher matchProperties(String id, String display, String value) { @@ -39,16 +38,7 @@ public class AuthorityEntryMatcher { hasJsonPath("$.id", is(id)), hasJsonPath("$.display", is(display)), hasJsonPath("$.value", is(value)), - hasJsonPath("$.type", is("authority")) - ); - } - - /** - * Gets a matcher for all expected embeds when the full projection is requested. - */ - public static Matcher matchFullEmbeds() { - return matchEmbeds( - "authorityEntries" + hasJsonPath("$.type", is("vocabularyEntryDetail")) ); } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/VocabularyMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/VocabularyMatcher.java new file mode 100644 index 0000000000..6e23560911 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/VocabularyMatcher.java @@ -0,0 +1,44 @@ +/** + * 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.app.rest.matcher; + +import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.is; + +import org.hamcrest.Matcher; + +/** + * + * + * @author mykhaylo + * + */ +public class VocabularyMatcher { + + private VocabularyMatcher() {} + + public static Matcher matchProperties(String id, String name, + boolean scrollable, boolean hierarchical) { + return allOf( + hasJsonPath("$.id", is(id)), + hasJsonPath("$.name", is(name)), + hasJsonPath("$.scrollable", is(scrollable)), + hasJsonPath("$.hierarchical", is(hierarchical)), + hasJsonPath("$.type", is("vocabulary")) + ); + } + + public static Matcher matchVocabularyEntry(String display, String value, String type) { + return allOf( + hasJsonPath("$.display", is(display)), + hasJsonPath("$.value", is(value)), + hasJsonPath("$.type", is(type)) + ); + } +} diff --git a/dspace/config/controlled-vocabularies/controlledvocabulary.xsd b/dspace/config/controlled-vocabularies/controlledvocabulary.xsd index 30fbb7f8ad..7a5defefbd 100644 --- a/dspace/config/controlled-vocabularies/controlledvocabulary.xsd +++ b/dspace/config/controlled-vocabularies/controlledvocabulary.xsd @@ -58,6 +58,7 @@ or refer to the Web site http://dspace-dev.dsi.uminho.pt. + diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index b4ee4b134f..88b2b06d83 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1486,7 +1486,7 @@ orcid.url = https://orcid.org/ ## eg: nsi, srsc. ## Each DSpaceControlledVocabulary plugin comes with three configuration options: # vocabulary.plugin._plugin_.hierarchy.store = # default: true -# vocabulary.plugin._plugin_.hierarchy.suggest = # default: true +# vocabulary.plugin._plugin_.hierarchy.suggest = # default: false # vocabulary.plugin._plugin_.delimiter = "" # default: "::" ## ## An example using "srsc" can be found later in this section