Skip to content

Commit bc719a0

Browse files
committed
[DURACOM-427] Fix on multilanguage vocabulary and value-pair
1 parent 5e96fe7 commit bc719a0

12 files changed

Lines changed: 1024 additions & 36 deletions

File tree

dspace-api/src/main/java/org/dspace/content/authority/DCInputAuthority.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
* fields.
5050
*/
5151
public class DCInputAuthority extends SelfNamedPlugin implements ChoiceAuthority {
52+
public static final String UNKNOWN_KEY = "UNKNOWN KEY ";
53+
5254
private static Logger log = org.apache.logging.log4j.LogManager.getLogger(DCInputAuthority.class);
5355

5456
/**
@@ -197,18 +199,21 @@ public String getLabel(String key, String locale) {
197199
locale = I18nUtil.getDefaultLocale().getLanguage();
198200
}
199201

202+
String[] valuesLocale = values.get(locale);
200203
String[] labelsLocale = labels.get(locale);
201204
int pos = -1;
202-
for (int i = 0; i < labelsLocale.length; i++) {
203-
if (labelsLocale[i].equals(key)) {
205+
// search in the values to return the label
206+
for (int i = 0; valuesLocale != null && i < valuesLocale.length; i++) {
207+
if (valuesLocale[i].equals(key)) {
204208
pos = i;
205209
break;
206210
}
207211
}
208-
if (pos != -1) {
212+
if (pos != -1 && labelsLocale != null) {
213+
// return the label in the same position where we found the value
209214
return labelsLocale[pos];
210215
} else {
211-
return "UNKNOWN KEY " + key;
216+
return UNKNOWN_KEY + key;
212217
}
213218
}
214219

dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,7 +1150,11 @@ private void resolveFacetFields(Context context, DiscoverQuery query, DiscoverRe
11501150
for (FacetField.Count facetValue : facetValues) {
11511151
String displayedValue = transformDisplayedValue(context, facetField.getName(),
11521152
facetValue.getName());
1153+
11531154
String field = transformFacetField(facetFieldConfig, facetField.getName(), true);
1155+
String currentLocalePrefix = context.getCurrentLocale().getLanguage() + "_";
1156+
field = StringUtils.removeStart(field, currentLocalePrefix);
1157+
11541158
String authorityValue = transformAuthorityValue(context, facetField.getName(),
11551159
facetValue.getName());
11561160
String sortValue = transformSortValue(context,
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
/**
2+
* The contents of this file are subject to the license and copyright
3+
* detailed in the LICENSE and NOTICE files at the root of the source
4+
* tree and available online at
5+
*
6+
* http://www.dspace.org/license/
7+
*/
8+
package org.dspace.discovery;
9+
10+
import static org.apache.commons.lang3.StringUtils.isNotBlank;
11+
import static org.dspace.discovery.SearchUtils.AUTHORITY_SEPARATOR;
12+
import static org.dspace.discovery.SearchUtils.FILTER_SEPARATOR;
13+
import static org.dspace.discovery.SolrServiceImpl.SOLR_FIELD_SUFFIX_FACET_PREFIXES;
14+
15+
import java.sql.SQLException;
16+
import java.util.List;
17+
import java.util.Locale;
18+
import java.util.stream.Collectors;
19+
20+
import org.apache.commons.lang3.StringUtils;
21+
import org.apache.solr.common.SolrInputDocument;
22+
import org.dspace.content.Collection;
23+
import org.dspace.content.Item;
24+
import org.dspace.content.MetadataValue;
25+
import org.dspace.content.authority.ChoiceAuthority;
26+
import org.dspace.content.authority.DCInputAuthority;
27+
import org.dspace.content.authority.DSpaceControlledVocabulary;
28+
import org.dspace.content.authority.service.ChoiceAuthorityService;
29+
import org.dspace.content.service.ItemService;
30+
import org.dspace.core.Constants;
31+
import org.dspace.core.Context;
32+
import org.dspace.core.I18nUtil;
33+
import org.dspace.core.exception.SQLRuntimeException;
34+
import org.dspace.discovery.configuration.DiscoveryConfiguration;
35+
import org.dspace.discovery.configuration.DiscoverySearchFilter;
36+
import org.dspace.discovery.configuration.MultiLanguageDiscoverSearchFilterFacet;
37+
import org.dspace.discovery.indexobject.IndexableItem;
38+
import org.dspace.services.ConfigurationService;
39+
import org.dspace.web.ContextUtil;
40+
import org.slf4j.Logger;
41+
import org.slf4j.LoggerFactory;
42+
import org.springframework.beans.factory.annotation.Autowired;
43+
44+
/**
45+
* Implementation of {@link SolrServiceIndexPlugin} that indexes controlled vocabulary
46+
* (value pairs) metadata fields into Solr for faceted search support.
47+
*
48+
* <p>This plugin processes metadata fields that have a Choice Authority configured (e.g.,
49+
* controlled vocabularies, authority-controlled fields) and adds multiple Solr fields
50+
* to enable flexible searching and filtering:
51+
* <ul>
52+
* <li>{@code <language>_<field>_keyword} - The value with authority suffix for keyword search</li>
53+
* <li>{@code <language>_<field>_acid} - Lowercase value with separator for acid search</li>
54+
* <li>{@code <language>_<field>_filter} - Lowercase value for filter queries</li>
55+
* <li>{@code <language>_<field>_facet} - Value with separator for faceting</li>
56+
* <li>{@code <language>_<field>_ac} - Autocomplete field</li>
57+
* <li>{@code <language>_<field>_authority} - Authority key when present</li>
58+
* </ul>
59+
*
60+
* <p>The plugin supports multilingual indexing by iterating through all configured locales
61+
* and creating language-specific field variants. It handles both {@link DSpaceControlledVocabulary}
62+
* and {@link DCInputAuthority} authority implementations.
63+
*
64+
* <p>Configuration: The separator character used in field values can be customized via
65+
* the {@code discovery.solr.facets.split.char} configuration property (defaults to
66+
* {@link SearchUtils#FILTER_SEPARATOR}).
67+
*
68+
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
69+
* @see SolrServiceIndexPlugin
70+
* @see DiscoverySearchFilter
71+
*/
72+
public class SolrServiceValuePairsIndexPlugin implements SolrServiceIndexPlugin {
73+
74+
private static final Logger LOGGER = LoggerFactory.getLogger(SolrServiceValuePairsIndexPlugin.class);
75+
76+
@Autowired
77+
private ItemService itemService;
78+
79+
@Autowired
80+
private ChoiceAuthorityService cas;
81+
82+
@Autowired
83+
private ConfigurationService configurationService;
84+
85+
/**
86+
* {@inheritDoc}
87+
*
88+
* <p>Indexes all controlled vocabulary metadata fields for the given item.
89+
* Only processes items that have a parent Collection with configured Choice Authorities.
90+
* For each metadata field with choices configured, adds language-specific Solr fields
91+
* for faceted search support.
92+
*
93+
* @param context the DSpace context
94+
* @param object the indexable object (must be an {@link IndexableItem})
95+
* @param document the Solr document to add fields to
96+
*/
97+
@Override
98+
@SuppressWarnings("rawtypes")
99+
public void additionalIndex(Context context, IndexableObject object, SolrInputDocument document) {
100+
if (isNotIndexableItem(object)) {
101+
return;
102+
}
103+
104+
Item item = ((IndexableItem)object).getIndexedObject();
105+
try {
106+
Collection collection = (Collection) itemService.getParentObject(context, item);
107+
for (MetadataValue metadata : item.getItemService().getMetadata(item, Item.ANY, Item.ANY, Item.ANY,
108+
Item.ANY)) {
109+
for (Locale locale : I18nUtil.getSupportedLocales()) {
110+
String language = locale.getLanguage();
111+
if (cas.isChoicesConfigured(metadata.getMetadataField().toString(), item.getType(), collection)) {
112+
additionalIndex(collection, item, metadata, language, document);
113+
}
114+
}
115+
}
116+
117+
} catch (Exception ex) {
118+
LOGGER.error("An error occurs indexing value pairs for item {}", item.getID(), ex);
119+
}
120+
}
121+
122+
/**
123+
* Adds discovery field values for a specific metadata value in a given language.
124+
*
125+
* @param collection the parent collection used to resolve the choice authority
126+
* @param item the item being indexed
127+
* @param metadataValue the metadata value to process
128+
* @param language the language code for the indexed fields
129+
* @param document the Solr document to add fields to
130+
*/
131+
private void additionalIndex(Collection collection, Item item, MetadataValue metadataValue, String language,
132+
SolrInputDocument document) {
133+
String metadataField = metadataValue.getMetadataField().toString('.');
134+
List<DiscoverySearchFilter> searchFilters = findSearchFiltersByMetadataField(item, metadataField);
135+
String authority = metadataValue.getAuthority();
136+
String value = getMetadataValue(collection, metadataValue, language);
137+
if (StringUtils.isNotBlank(value)) {
138+
for (DiscoverySearchFilter searchFilter : searchFilters) {
139+
addDiscoveryFieldFields(language, document, value, authority, searchFilter);
140+
}
141+
}
142+
}
143+
144+
/**
145+
* Resolves the display label for a metadata value based on its authority.
146+
*
147+
* <p>For controlled vocabularies, returns the authority label if available,
148+
* otherwise falls back to the raw metadata value. For DCInputAuthority fields,
149+
* always attempts to resolve the label from the authority.
150+
*
151+
* @param collection the parent collection used to resolve the choice authority
152+
* @param metadataValue the metadata value to resolve
153+
* @param language the language for label resolution
154+
* @return the display label, or null if no suitable label can be found
155+
*/
156+
private String getMetadataValue(Collection collection, MetadataValue metadataValue, String language) {
157+
String fieldKey = metadataValue.getMetadataField().toString();
158+
ChoiceAuthority choiceAuthority = cas.getAuthorityByFieldKeyCollection(fieldKey, Constants.ITEM, collection);
159+
String authority = metadataValue.getAuthority();
160+
if (choiceAuthority instanceof DSpaceControlledVocabulary) {
161+
String label = StringUtils.isNotBlank(authority) ? choiceAuthority.getLabel(authority, language)
162+
: metadataValue.getValue();
163+
if (StringUtils.isBlank(label)) {
164+
label = metadataValue.getValue();
165+
}
166+
return label;
167+
} else if (choiceAuthority instanceof DCInputAuthority) {
168+
String label = choiceAuthority.getLabel(metadataValue.getValue(), language);
169+
if (StringUtils.isBlank(label)) {
170+
label = metadataValue.getValue();
171+
}
172+
return label;
173+
}
174+
return null;
175+
}
176+
177+
/**
178+
* Adds multiple Solr fields for a discovery search filter value.
179+
*
180+
* <p>Creates the following fields (with language prefix):
181+
* <ul>
182+
* <li>{@code _keyword} - Value with optional authority suffix for keyword search</li>
183+
* <li>{@code _acid} - Lowercase value with separator for acid search</li>
184+
* <li>{@code _filter} - Lowercase value for filter queries</li>
185+
* <li>{@code _facet} - Value with separator for faceting (uses {@link SolrServiceImpl#SOLR_FIELD_SUFFIX_FACET_PREFIXES})</li>
186+
* <li>{@code _ac} - Lowercase value with separator for autocomplete</li>
187+
* <li>{@code _authority} - The authority key if present and existing authority field exists</li>
188+
* </ul>
189+
*
190+
* @param language the language code to prefix field names with
191+
* @param document the Solr document to add fields to
192+
* @param value the display value to index
193+
* @param authority the authority key (may be null or blank)
194+
* @param searchFilter the discovery search filter configuration
195+
*/
196+
private void addDiscoveryFieldFields(String language, SolrInputDocument document, String value, String authority,
197+
DiscoverySearchFilter searchFilter) {
198+
String separator = configurationService.getProperty("discovery.solr.facets.split.char", FILTER_SEPARATOR);
199+
String fieldNameWithLanguage = language + "_" + searchFilter.getIndexFieldName();
200+
String valueLowerCase = value.toLowerCase();
201+
202+
String keywordField = appendAuthorityIfNotBlank(value, authority);
203+
String acidField = appendAuthorityIfNotBlank(valueLowerCase + separator + value, authority);
204+
String filterField = appendAuthorityIfNotBlank(valueLowerCase + separator + value, authority);
205+
String prefixField = appendAuthorityIfNotBlank(valueLowerCase + separator + value, authority);
206+
207+
document.addField(fieldNameWithLanguage + "_keyword", keywordField);
208+
document.addField(fieldNameWithLanguage + "_acid", acidField);
209+
document.addField(fieldNameWithLanguage + "_filter", filterField);
210+
document.addField(fieldNameWithLanguage + SOLR_FIELD_SUFFIX_FACET_PREFIXES, prefixField);
211+
document.addField(fieldNameWithLanguage + "_ac", valueLowerCase + separator + value);
212+
if (document.containsKey(searchFilter.getIndexFieldName() + "_authority")) {
213+
document.addField(fieldNameWithLanguage + "_authority", authority);
214+
}
215+
}
216+
217+
/**
218+
* Appends the authority separator and authority key to a field value if the authority
219+
* is not blank.
220+
*
221+
* @param fieldValue the base field value
222+
* @param authority the authority key (may be null or blank)
223+
* @return the field value with authority appended if present, otherwise just the field value
224+
*/
225+
private String appendAuthorityIfNotBlank(String fieldValue, String authority) {
226+
return isNotBlank(authority) ? fieldValue + AUTHORITY_SEPARATOR + authority : fieldValue;
227+
}
228+
229+
/**
230+
* Returns all the search fields configured for the given metadataField. Filters
231+
* returned are not filtered by instance type equal to
232+
* {@link MultiLanguageDiscoverSearchFilterFacet} to allow for language-based
233+
* searches.
234+
*
235+
* @param item the item being indexed (used to resolve discovery configuration)
236+
* @param metadataField the metadata field key to search for (e.g., "dc.subject")
237+
* @return list of discovery search filters that include this metadata field
238+
*/
239+
private List<DiscoverySearchFilter> findSearchFiltersByMetadataField(Item item, String metadataField) {
240+
return getAllDiscoveryConfiguration(item).stream()
241+
.flatMap(discoveryConfiguration -> discoveryConfiguration.getSearchFilters().stream())
242+
.filter(searchFilter -> searchFilter.getMetadataFields().contains(metadataField))
243+
.distinct()
244+
.collect(Collectors.toList());
245+
}
246+
247+
/**
248+
* Retrieves all discovery configurations associated with an item.
249+
*
250+
* @param item the item to get configurations for
251+
* @return list of discovery configurations for the item's collections
252+
* @throws SQLRuntimeException if a database error occurs
253+
*/
254+
private List<DiscoveryConfiguration> getAllDiscoveryConfiguration(Item item) {
255+
try {
256+
return SearchUtils.getAllDiscoveryConfigurations(ContextUtil.obtainCurrentRequestContext(), item);
257+
} catch (SQLException e) {
258+
throw new SQLRuntimeException(e);
259+
}
260+
}
261+
262+
/**
263+
* Checks whether the given indexable object is not an indexable item.
264+
*
265+
* @param object the indexable object to check
266+
* @return true if the object is not an IndexableItem instance
267+
*/
268+
@SuppressWarnings("rawtypes")
269+
private boolean isNotIndexableItem(IndexableObject object) {
270+
return !(object instanceof IndexableItem);
271+
}
272+
273+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* The contents of this file are subject to the license and copyright
3+
* detailed in the LICENSE and NOTICE files at the root of the source
4+
* tree and available online at
5+
*
6+
* http://www.dspace.org/license/
7+
*/
8+
package org.dspace.discovery.configuration;
9+
10+
/**
11+
* Extension of {@link DiscoverySearchFilterFacet} for multi-language support.
12+
*
13+
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
14+
*
15+
*/
16+
public class MultiLanguageDiscoverSearchFilterFacet extends DiscoverySearchFilterFacet {
17+
}

dspace-api/src/main/java/org/dspace/discovery/utils/DiscoverQueryBuilder.java

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.dspace.discovery.configuration.DiscoverySearchFilterFacet;
3838
import org.dspace.discovery.configuration.DiscoverySortConfiguration;
3939
import org.dspace.discovery.configuration.DiscoverySortFieldConfiguration;
40+
import org.dspace.discovery.configuration.MultiLanguageDiscoverSearchFilterFacet;
4041
import org.dspace.discovery.indexobject.factory.IndexFactory;
4142
import org.dspace.discovery.utils.parameter.QueryBuilderSearchFilter;
4243
import org.dspace.services.ConfigurationService;
@@ -242,12 +243,17 @@ private void fillFacetIntoQueryArgs(Context context, IndexableObject scope, Stri
242243

243244
} else {
244245

246+
String indexFieldName = facet.getIndexFieldName();
247+
if (facet instanceof MultiLanguageDiscoverSearchFilterFacet) {
248+
indexFieldName = context.getCurrentLocale().getLanguage() + "_" + indexFieldName;
249+
}
250+
245251
//Add one to our facet limit to make sure that if we have more then the shown facets that we show our
246252
// "show more" url
247253
int facetLimit = pageSize + 1;
248254
//This should take care of the sorting for us
249255
prefix = StringUtils.isNotBlank(prefix) ? prefix.toLowerCase() : null;
250-
queryArgs.addFacetField(new DiscoverFacetField(facet.getIndexFieldName(), facet.getType(), facetLimit,
256+
queryArgs.addFacetField(new DiscoverFacetField(indexFieldName, facet.getType(), facetLimit,
251257
facet.getSortOrderSidebar(),
252258
StringUtils.trimToNull(prefix)));
253259
}
@@ -408,11 +414,19 @@ private String[] convertFiltersToString(Context context, DiscoveryConfiguration
408414
throw new IllegalArgumentException(searchFilter.getName() + " is not a valid search filter");
409415
}
410416

411-
DiscoverFilterQuery filterQuery = searchService.toFilterQuery(context,
412-
filter.getIndexFieldName(),
413-
searchFilter.getOperator(),
414-
searchFilter.getValue(),
415-
discoveryConfiguration);
417+
String field = filter.getIndexFieldName();
418+
if (filter instanceof MultiLanguageDiscoverSearchFilterFacet) {
419+
field = context.getCurrentLocale().getLanguage() + "_" + field;
420+
}
421+
422+
DiscoverFilterQuery filterQuery =
423+
searchService.toFilterQuery(
424+
context,
425+
field,
426+
searchFilter.getOperator(),
427+
searchFilter.getValue(),
428+
discoveryConfiguration
429+
);
416430

417431
if (filterQuery != null) {
418432
filterQueries.add(filterQuery.getFilterQuery());

0 commit comments

Comments
 (0)