Browse-by support for controlled vocabularies

https://github.com/DSpace/RestContract/pull/225
This commit is contained in:
Marie Verdonck
2023-05-04 20:11:47 +02:00
parent 4cdb66267e
commit 66eb8a548f
13 changed files with 286 additions and 30 deletions

View File

@@ -15,6 +15,7 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
@@ -30,6 +31,8 @@ import org.dspace.content.MetadataValue;
import org.dspace.content.authority.service.ChoiceAuthorityService;
import org.dspace.core.Utils;
import org.dspace.core.service.PluginService;
import org.dspace.discovery.configuration.DiscoveryConfigurationService;
import org.dspace.discovery.configuration.DiscoverySearchFilterFacet;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired;
@@ -80,6 +83,9 @@ public final class ChoiceAuthorityServiceImpl implements ChoiceAuthorityService
protected Map<String, Map<String, List<String>>> authoritiesFormDefinitions =
new HashMap<String, Map<String, List<String>>>();
// Map of vocabulary authorities to and their index info equivalent
protected Map<String, DSpaceControlledVocabularyIndex> vocabularyIndexMap = new HashMap<>();
// the item submission reader
private SubmissionConfigReader itemSubmissionConfigReader;
@@ -87,6 +93,8 @@ public final class ChoiceAuthorityServiceImpl implements ChoiceAuthorityService
protected ConfigurationService configurationService;
@Autowired(required = true)
protected PluginService pluginService;
@Autowired
private DiscoveryConfigurationService searchConfigurationService;
final static String CHOICES_PLUGIN_PREFIX = "choices.plugin.";
final static String CHOICES_PRESENTATION_PREFIX = "choices.presentation.";
@@ -540,4 +548,50 @@ public final class ChoiceAuthorityServiceImpl implements ChoiceAuthorityService
HierarchicalAuthority ma = (HierarchicalAuthority) getChoiceAuthorityByAuthorityName(authorityName);
return ma.getParentChoice(authorityName, vocabularyId, locale);
}
@Override
public DSpaceControlledVocabularyIndex getVocabularyIndex(String nameVocab) {
if (this.vocabularyIndexMap.containsKey(nameVocab)) {
return this.vocabularyIndexMap.get(nameVocab);
} else {
init();
ChoiceAuthority source = this.getChoiceAuthorityByAuthorityName(nameVocab);
if (source != null && source instanceof DSpaceControlledVocabulary) {
Set<String> metadataFields = new HashSet<>();
Map<String, List<String>> formsToFields = this.authoritiesFormDefinitions.get(nameVocab);
for (Map.Entry<String, List<String>> formToField : formsToFields.entrySet()) {
metadataFields.addAll(formToField.getValue().stream().map(value ->
StringUtils.replace(value, "_", "."))
.collect(Collectors.toList()));
}
DiscoverySearchFilterFacet matchingFacet = null;
for (DiscoverySearchFilterFacet facetConfig : searchConfigurationService.getAllFacetsConfig()) {
boolean coversAllFieldsFromVocab = true;
for (String fieldFromVocab: metadataFields) {
boolean coversFieldFromVocab = false;
for (String facetMdField: facetConfig.getMetadataFields()) {
if (facetMdField.startsWith(fieldFromVocab)) {
coversFieldFromVocab = true;
break;
}
}
if (!coversFieldFromVocab) {
coversAllFieldsFromVocab = false;
break;
}
}
if (coversAllFieldsFromVocab) {
matchingFacet = facetConfig;
break;
}
}
DSpaceControlledVocabularyIndex vocabularyIndex =
new DSpaceControlledVocabularyIndex((DSpaceControlledVocabulary) source, metadataFields,
matchingFacet);
this.vocabularyIndexMap.put(nameVocab, vocabularyIndex);
return vocabularyIndex;
}
return null;
}
}
}

View File

@@ -0,0 +1,45 @@
/**
* 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.Set;
import org.dspace.discovery.configuration.DiscoverySearchFilterFacet;
/**
* Helper class to transform a {@link org.dspace.content.authority.DSpaceControlledVocabulary} into a
* {@code BrowseIndexRest}
* cached by {@link org.dspace.content.authority.service.ChoiceAuthorityService#getVocabularyIndex(String)}
*
* @author Marie Verdonck (Atmire) on 04/05/2023
*/
public class DSpaceControlledVocabularyIndex {
protected DSpaceControlledVocabulary vocabulary;
protected Set<String> metadataFields;
protected DiscoverySearchFilterFacet facetConfig;
public DSpaceControlledVocabularyIndex(DSpaceControlledVocabulary controlledVocabulary, Set<String> metadataFields,
DiscoverySearchFilterFacet facetConfig) {
this.vocabulary = controlledVocabulary;
this.metadataFields = metadataFields;
this.facetConfig = facetConfig;
}
public DSpaceControlledVocabulary getVocabulary() {
return vocabulary;
}
public Set<String> getMetadataFields() {
return this.metadataFields;
}
public DiscoverySearchFilterFacet getFacetConfig() {
return this.facetConfig;
}
}

View File

@@ -15,6 +15,7 @@ import org.dspace.content.MetadataValue;
import org.dspace.content.authority.Choice;
import org.dspace.content.authority.ChoiceAuthority;
import org.dspace.content.authority.Choices;
import org.dspace.content.authority.DSpaceControlledVocabularyIndex;
/**
* Broker for ChoiceAuthority plugins, and for other information configured
@@ -220,4 +221,7 @@ public interface ChoiceAuthorityService {
* @return the parent Choice object if any
*/
public Choice getParentChoice(String authorityName, String vocabularyId, String locale);
public DSpaceControlledVocabularyIndex getVocabularyIndex(String nameVocab);
}

View File

@@ -92,6 +92,18 @@ public class DiscoveryConfigurationService {
return configs;
}
/**
* @return All configurations for {@link org.dspace.discovery.configuration.DiscoverySearchFilterFacet}
*/
public List<DiscoverySearchFilterFacet> getAllFacetsConfig() {
List<DiscoverySearchFilterFacet> configs = new ArrayList<>();
for (String key : map.keySet()) {
DiscoveryConfiguration config = map.get(key);
configs.addAll(config.getSidebarFacets());
}
return configs;
}
public static void main(String[] args) {
System.out.println(DSpaceServicesFactory.getInstance().getServiceManager().getServicesNames().size());
DiscoveryConfigurationService mainService = DSpaceServicesFactory.getInstance().getServiceManager()

View File

@@ -7,6 +7,9 @@
*/
package org.dspace.app.rest.converter;
import static org.dspace.app.rest.model.BrowseIndexRest.BROWSE_TYPE_FLAT;
import static org.dspace.app.rest.model.BrowseIndexRest.BROWSE_TYPE_VALUE_LIST;
import java.util.ArrayList;
import java.util.List;
@@ -33,14 +36,15 @@ public class BrowseIndexConverter implements DSpaceConverter<BrowseIndex, Browse
bir.setId(obj.getName());
bir.setDataType(obj.getDataType());
bir.setOrder(obj.getDefaultOrder());
bir.setMetadataBrowse(obj.isMetadataIndex());
List<String> metadataList = new ArrayList<String>();
if (obj.isMetadataIndex()) {
for (String s : obj.getMetadata().split(",")) {
metadataList.add(s.trim());
}
bir.setBrowseType(BROWSE_TYPE_VALUE_LIST);
} else {
metadataList.add(obj.getSortOption().getMetadata());
bir.setBrowseType(BROWSE_TYPE_FLAT);
}
bir.setMetadataList(metadataList);

View File

@@ -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.app.rest.converter;
import java.util.ArrayList;
import org.dspace.app.rest.model.BrowseIndexRest;
import org.dspace.app.rest.projection.Projection;
import org.dspace.content.authority.DSpaceControlledVocabularyIndex;
import org.springframework.stereotype.Component;
/**
* This is the converter from a {@link org.dspace.content.authority.DSpaceControlledVocabularyIndex} to a
* {@link org.dspace.app.rest.model.BrowseIndexRest#BROWSE_TYPE_HIERARCHICAL} {@link org.dspace.app.rest.model.BrowseIndexRest}
*
* @author Marie Verdonck (Atmire) on 04/05/2023
*/
@Component
public class HierarchicalBrowseConverter implements DSpaceConverter<DSpaceControlledVocabularyIndex, BrowseIndexRest> {
@Override
public BrowseIndexRest convert(DSpaceControlledVocabularyIndex obj, Projection projection) {
BrowseIndexRest bir = new BrowseIndexRest();
bir.setProjection(projection);
bir.setId(obj.getVocabulary().getPluginInstanceName());
bir.setBrowseType(BrowseIndexRest.BROWSE_TYPE_HIERARCHICAL);
bir.setFacetType(obj.getFacetConfig().getIndexFieldName());
bir.setVocabulary(obj.getVocabulary().getPluginInstanceName());
bir.setMetadataList(new ArrayList<>(obj.getMetadataFields()));
return bir;
}
@Override
public Class<DSpaceControlledVocabularyIndex> getModelClass() {
return DSpaceControlledVocabularyIndex.class;
}
}

View File

@@ -37,11 +37,11 @@ public class BrowseEntryHalLinkFactory extends HalLinkFactory<BrowseEntryResourc
UriComponentsBuilder baseLink = uriBuilder(
getMethodOn(bix.getCategory(), bix.getType()).findRel(null, null, bix.getCategory(),
English.plural(bix.getType()), bix.getId(),
BrowseIndexRest.ITEMS, null, null));
BrowseIndexRest.LINK_ITEMS, null, null));
addFilterParams(baseLink, data);
list.add(buildLink(BrowseIndexRest.ITEMS,
list.add(buildLink(BrowseIndexRest.LINK_ITEMS,
baseLink.build().encode().toUriString()));
}
}

View File

@@ -10,6 +10,7 @@ package org.dspace.app.rest.model;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.dspace.app.rest.RestResourceController;
@@ -20,11 +21,11 @@ import org.dspace.app.rest.RestResourceController;
*/
@LinksRest(links = {
@LinkRest(
name = BrowseIndexRest.ITEMS,
name = BrowseIndexRest.LINK_ITEMS,
method = "listBrowseItems"
),
@LinkRest(
name = BrowseIndexRest.ENTRIES,
name = BrowseIndexRest.LINK_ENTRIES,
method = "listBrowseEntries"
)
})
@@ -35,20 +36,38 @@ public class BrowseIndexRest extends BaseObjectRest<String> {
public static final String CATEGORY = RestAddressableModel.DISCOVER;
public static final String ITEMS = "items";
public static final String ENTRIES = "entries";
public static final String LINK_ITEMS = "items";
public static final String LINK_ENTRIES = "entries";
public static final String LINK_VOCABULARY = "vocabulary";
boolean metadataBrowse;
// if the browse index has two levels, the 1st level shows the list of entries like author names, subjects, types,
// etc. the second level is the actual list of items linked to a specific entry
public static final String BROWSE_TYPE_VALUE_LIST = "valueList";
// if the browse index has one level: the full list of items
public static final String BROWSE_TYPE_FLAT = "flatBrowse";
// if the browse index should display the vocabulary tree. The 1st level shows the tree.
// The second level is the actual list of items linked to a specific entry
public static final String BROWSE_TYPE_HIERARCHICAL = "hierarchicalBrowse";
// Shared fields
String browseType;
@JsonProperty(value = "metadata")
List<String> metadataList;
// Single browse index fields
@JsonInclude(JsonInclude.Include.NON_NULL)
String dataType;
@JsonInclude(JsonInclude.Include.NON_NULL)
List<SortOption> sortOptions;
@JsonInclude(JsonInclude.Include.NON_NULL)
String order;
// Hierarchical browse fields
@JsonInclude(JsonInclude.Include.NON_NULL)
String facetType;
@JsonInclude(JsonInclude.Include.NON_NULL)
String vocabulary;
@JsonIgnore
@Override
public String getCategory() {
@@ -60,14 +79,6 @@ public class BrowseIndexRest extends BaseObjectRest<String> {
return NAME;
}
public boolean isMetadataBrowse() {
return metadataBrowse;
}
public void setMetadataBrowse(boolean metadataBrowse) {
this.metadataBrowse = metadataBrowse;
}
public List<String> getMetadataList() {
return metadataList;
}
@@ -100,6 +111,38 @@ public class BrowseIndexRest extends BaseObjectRest<String> {
this.sortOptions = sortOptions;
}
/**
* - valueList => if the browse index has two levels, the 1st level shows the list of entries like author names,
* subjects, types, etc. the second level is the actual list of items linked to a specific entry
* - flatBrowse if the browse index has one level: the full list of items
* - hierarchicalBrowse if the browse index should display the vocabulary tree. The 1st level shows the tree.
* The second level is the actual list of items linked to a specific entry
*/
public void setBrowseType(String browseType) {
this.browseType = browseType;
}
public String getBrowseType() {
return browseType;
}
public void setFacetType(String facetType) {
this.facetType = facetType;
}
public String getFacetType() {
return facetType;
}
public void setVocabulary(String vocabulary) {
this.vocabulary = vocabulary;
}
public String getVocabulary() {
return vocabulary;
}
@Override
public Class getController() {
return RestResourceController.class;

View File

@@ -7,9 +7,20 @@
*/
package org.dspace.app.rest.model.hateoas;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
import org.atteo.evo.inflector.English;
import org.dspace.app.rest.RestResourceController;
import org.dspace.app.rest.model.BrowseIndexRest;
import org.dspace.app.rest.model.VocabularyRest;
import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource;
import org.dspace.app.rest.utils.Utils;
import org.dspace.content.authority.ChoiceAuthority;
import org.dspace.content.authority.factory.ContentAuthorityServiceFactory;
import org.dspace.content.authority.service.ChoiceAuthorityService;
import org.springframework.hateoas.Link;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Browse Index Rest HAL Resource. The HAL Resource wraps the REST Resource
@@ -19,15 +30,32 @@ import org.dspace.app.rest.utils.Utils;
*/
@RelNameDSpaceResource(BrowseIndexRest.NAME)
public class BrowseIndexResource extends DSpaceResource<BrowseIndexRest> {
public BrowseIndexResource(BrowseIndexRest bix, Utils utils) {
super(bix, utils);
// TODO: the following code will force the embedding of items and
// entries in the browseIndex we need to find a way to populate the rels
// array from the request/projection right now it is always null
// super(bix, utils, "items", "entries");
if (bix.isMetadataBrowse()) {
add(utils.linkToSubResource(bix, BrowseIndexRest.ENTRIES));
if (bix.getBrowseType().equals(BrowseIndexRest.BROWSE_TYPE_VALUE_LIST)) {
add(utils.linkToSubResource(bix, BrowseIndexRest.LINK_ENTRIES));
add(utils.linkToSubResource(bix, BrowseIndexRest.LINK_ITEMS));
}
if (bix.getBrowseType().equals(BrowseIndexRest.BROWSE_TYPE_FLAT)) {
add(utils.linkToSubResource(bix, BrowseIndexRest.LINK_ITEMS));
}
if (bix.getBrowseType().equals(BrowseIndexRest.BROWSE_TYPE_HIERARCHICAL)) {
ChoiceAuthorityService choiceAuthorityService =
ContentAuthorityServiceFactory.getInstance().getChoiceAuthorityService();
ChoiceAuthority source = choiceAuthorityService.getChoiceAuthorityByAuthorityName(bix.getVocabulary());
UriComponentsBuilder baseLink = linkTo(
methodOn(RestResourceController.class, VocabularyRest.AUTHENTICATION).findRel(null,
null, VocabularyRest.CATEGORY,
English.plural(VocabularyRest.NAME), source.getPluginInstanceName(),
"", null, null)).toUriComponentsBuilder();
add(Link.of(baseLink.build().encode().toUriString(), BrowseIndexRest.LINK_VOCABULARY));
}
add(utils.linkToSubResource(bix, BrowseIndexRest.ITEMS));
}
}

View File

@@ -40,7 +40,7 @@ import org.springframework.stereotype.Component;
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*/
@Component(BrowseIndexRest.CATEGORY + "." + BrowseIndexRest.NAME + "." + BrowseIndexRest.ENTRIES)
@Component(BrowseIndexRest.CATEGORY + "." + BrowseIndexRest.NAME + "." + BrowseIndexRest.LINK_ENTRIES)
public class BrowseEntryLinkRepository extends AbstractDSpaceRestRepository
implements LinkRestRepository {
@@ -127,7 +127,8 @@ public class BrowseEntryLinkRepository extends AbstractDSpaceRestRepository
@Override
public boolean isEmbeddableRelation(Object data, String name) {
BrowseIndexRest bir = (BrowseIndexRest) data;
if (bir.isMetadataBrowse() && "entries".equals(name)) {
if (bir.getBrowseType().equals(BrowseIndexRest.BROWSE_TYPE_VALUE_LIST) &&
name.equals(BrowseIndexRest.LINK_ENTRIES)) {
return true;
}
return false;

View File

@@ -13,7 +13,10 @@ import java.util.List;
import org.dspace.app.rest.model.BrowseIndexRest;
import org.dspace.browse.BrowseException;
import org.dspace.browse.BrowseIndex;
import org.dspace.content.authority.DSpaceControlledVocabularyIndex;
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.Pageable;
import org.springframework.security.access.prepost.PreAuthorize;
@@ -27,20 +30,39 @@ import org.springframework.stereotype.Component;
@Component(BrowseIndexRest.CATEGORY + "." + BrowseIndexRest.NAME)
public class BrowseIndexRestRepository extends DSpaceRestRepository<BrowseIndexRest, String> {
@Autowired
private ChoiceAuthorityService choiceAuthorityService;
@Override
@PreAuthorize("permitAll()")
public BrowseIndexRest findOne(Context context, String name) {
BrowseIndexRest bi = null;
BrowseIndexRest bi = createFromMatchingBrowseIndex(name);
if (bi == null) {
bi = createFromMatchingVocabulary(name);
}
return bi;
}
private BrowseIndexRest createFromMatchingVocabulary(String name) {
DSpaceControlledVocabularyIndex vocabularyIndex = choiceAuthorityService.getVocabularyIndex(name);
if (vocabularyIndex != null) {
return converter.toRest(vocabularyIndex, utils.obtainProjection());
}
return null;
}
private BrowseIndexRest createFromMatchingBrowseIndex(String name) {
BrowseIndex bix;
try {
bix = BrowseIndex.getBrowseIndex(name);
bix = BrowseIndex.getBrowseIndex(name);
} catch (BrowseException e) {
throw new RuntimeException(e.getMessage(), e);
}
if (bix != null) {
bi = converter.toRest(bix, utils.obtainProjection());
return converter.toRest(bix, utils.obtainProjection());
}
return bi;
return null;
}
@Override

View File

@@ -42,7 +42,7 @@ import org.springframework.stereotype.Component;
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*/
@Component(BrowseIndexRest.CATEGORY + "." + BrowseIndexRest.NAME + "." + BrowseIndexRest.ITEMS)
@Component(BrowseIndexRest.CATEGORY + "." + BrowseIndexRest.NAME + "." + BrowseIndexRest.LINK_ITEMS)
public class BrowseItemLinkRepository extends AbstractDSpaceRestRepository
implements LinkRestRepository {
@@ -155,7 +155,8 @@ public class BrowseItemLinkRepository extends AbstractDSpaceRestRepository
@Override
public boolean isEmbeddableRelation(Object data, String name) {
BrowseIndexRest bir = (BrowseIndexRest) data;
if (!bir.isMetadataBrowse() && "items".equals(name)) {
if (bir.getBrowseType().equals(BrowseIndexRest.BROWSE_TYPE_FLAT) &&
name.equals(BrowseIndexRest.LINK_ITEMS)) {
return true;
}
return false;

View File

@@ -53,7 +53,7 @@ public class VocabularyRestRepository extends DSpaceRestRepository<VocabularyRes
@Autowired
private MetadataFieldService metadataFieldService;
@PreAuthorize("hasAuthority('AUTHENTICATED')")
@PreAuthorize("permitAll()")
@Override
public VocabularyRest findOne(Context context, String name) {
ChoiceAuthority source = cas.getChoiceAuthorityByAuthorityName(name);