Skip to content

Commit

Permalink
Merge pull request DSpace#9318 from leuphana/issue-9317
Browse files Browse the repository at this point in the history
Controlled Vocabulary: Provide ability to store the id of a controlled vocabulary node and offer localized labels using cv.xml files per locale
  • Loading branch information
tdonohue authored Dec 10, 2024
2 parents d08bbaf + 4e541da commit be92570
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package org.dspace.content.authority;

import java.io.File;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
Expand Down Expand Up @@ -65,14 +66,17 @@ public class DSpaceControlledVocabulary extends SelfNamedPlugin implements Hiera
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 idAttribute = "id";
protected static String labelAttribute = "label";
protected static String pluginNames[] = null;

protected String vocabularyName = null;
protected InputSource vocabulary = null;
protected Boolean suggestHierarchy = false;
protected Boolean storeHierarchy = true;
protected String hierarchyDelimiter = "::";
protected Integer preloadLevel = 1;
protected String valueAttribute = labelAttribute;
protected String valueTemplate = labelTemplate;

public DSpaceControlledVocabulary() {
super();
Expand Down Expand Up @@ -115,7 +119,7 @@ public boolean accept(File dir, String name) {
}
}

protected void init() {
protected void init(String locale) {
if (vocabulary == null) {
ConfigurationService config = DSpaceServicesFactory.getInstance().getConfigurationService();

Expand All @@ -125,13 +129,25 @@ protected void init() {
File.separator + "controlled-vocabularies" + File.separator;
String configurationPrefix = "vocabulary.plugin." + vocabularyName;
storeHierarchy = config.getBooleanProperty(configurationPrefix + ".hierarchy.store", storeHierarchy);
boolean storeIDs = config.getBooleanProperty(configurationPrefix + ".storeIDs", false);
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("(^\"|\"$)", "");
}
if (storeIDs) {
valueAttribute = idAttribute;
valueTemplate = idTemplate;
}

String filename = vocabulariesPath + vocabularyName + ".xml";
if (StringUtils.isNotEmpty(locale)) {
String localizedFilename = vocabulariesPath + vocabularyName + "_" + locale + ".xml";
if (Paths.get(localizedFilename).toFile().exists()) {
filename = localizedFilename;
}
}
log.info("Loading " + filename);
vocabulary = new InputSource(filename);
}
Expand All @@ -144,9 +160,9 @@ protected String buildString(Node node) {
return ("");
} else {
String parentValue = buildString(node.getParentNode());
Node currentLabel = node.getAttributes().getNamedItem("label");
if (currentLabel != null) {
String currentValue = currentLabel.getNodeValue();
Node currentNodeValue = node.getAttributes().getNamedItem(valueAttribute);
if (currentNodeValue != null) {
String currentValue = currentNodeValue.getNodeValue();
if (parentValue.equals("")) {
return currentValue;
} else {
Expand All @@ -160,12 +176,13 @@ protected String buildString(Node node) {

@Override
public Choices getMatches(String text, int start, int limit, String locale) {
init();
init(locale);
log.debug("Getting matches for '" + text + "'");
String xpathExpression = "";
String[] textHierarchy = text.split(hierarchyDelimiter, -1);
for (int i = 0; i < textHierarchy.length; i++) {
xpathExpression += String.format(xpathTemplate, textHierarchy[i].replaceAll("'", "&apos;").toLowerCase());
xpathExpression +=
String.format(xpathTemplate, textHierarchy[i].replaceAll("'", "&apos;").toLowerCase());
}
XPath xpath = XPathFactory.newInstance().newXPath();
int total = 0;
Expand All @@ -184,12 +201,13 @@ public Choices getMatches(String text, int start, int limit, String locale) {

@Override
public Choices getBestMatch(String text, String locale) {
init();
init(locale);
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("'", "&apos;"));
xpathExpression +=
String.format(valueTemplate, textHierarchy[i].replaceAll("'", "&apos;"));
}
XPath xpath = XPathFactory.newInstance().newXPath();
List<Choice> choices = new ArrayList<Choice>();
Expand All @@ -205,19 +223,19 @@ public Choices getBestMatch(String text, String locale) {

@Override
public String getLabel(String key, String locale) {
return getNodeLabel(key, this.suggestHierarchy);
return getNodeValue(key, locale, this.suggestHierarchy);
}

@Override
public String getValue(String key, String locale) {
return getNodeLabel(key, this.storeHierarchy);
return getNodeValue(key, locale, this.storeHierarchy);
}

@Override
public Choice getChoice(String authKey, String locale) {
Node node;
try {
node = getNode(authKey);
node = getNode(authKey, locale);
} catch (XPathExpressionException e) {
return null;
}
Expand All @@ -226,27 +244,27 @@ public Choice getChoice(String authKey, String locale) {

@Override
public boolean isHierarchical() {
init();
init(null);
return true;
}

@Override
public Choices getTopChoices(String authorityName, int start, int limit, String locale) {
init();
init(locale);
String xpathExpression = rootTemplate;
return getChoicesByXpath(xpathExpression, start, limit);
}

@Override
public Choices getChoicesByParent(String authorityName, String parentId, int start, int limit, String locale) {
init();
init(locale);
String xpathExpression = String.format(idTemplate, parentId);
return getChoicesByXpath(xpathExpression, start, limit);
}

@Override
public Choice getParentChoice(String authorityName, String childId, String locale) {
init();
init(locale);
try {
String xpathExpression = String.format(idParentTemplate, childId);
Choice choice = createChoiceFromNode(getNodeFromXPath(xpathExpression));
Expand All @@ -259,7 +277,7 @@ public Choice getParentChoice(String authorityName, String childId, String local

@Override
public Integer getPreloadLevel() {
init();
init(null);
return preloadLevel;
}

Expand All @@ -270,8 +288,8 @@ private boolean isRootElement(Node node) {
return false;
}

private Node getNode(String key) throws XPathExpressionException {
init();
private Node getNode(String key, String locale) throws XPathExpressionException {
init(locale);
String xpathExpression = String.format(idTemplate, key);
Node node = getNodeFromXPath(xpathExpression);
return node;
Expand Down Expand Up @@ -319,16 +337,16 @@ private Map<String, String> addOtherInformation(String parentCurr, String noteCu
return extras;
}

private String getNodeLabel(String key, boolean useHierarchy) {
private String getNodeValue(String key, String locale, boolean useHierarchy) {
try {
Node node = getNode(key);
Node node = getNode(key, locale);
if (Objects.isNull(node)) {
return null;
}
if (useHierarchy) {
return this.buildString(node);
} else {
return node.getAttributes().getNamedItem("label").getNodeValue();
return node.getAttributes().getNamedItem(valueAttribute).getNodeValue();
}
} catch (XPathExpressionException e) {
return ("");
Expand All @@ -349,7 +367,7 @@ private String getValue(Node node) {
if (this.storeHierarchy) {
return hierarchy;
} else {
return node.getAttributes().getNamedItem("label").getNodeValue();
return node.getAttributes().getNamedItem(valueAttribute).getNodeValue();
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<node id='Countries' label='Countries'>
<isComposedBy>
<node id='Africa' label='Africa'>
<isComposedBy>
<node id='DZA' label='Algeria'/>
</isComposedBy>
</node>
</isComposedBy>
</node>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<node id='Countries' label='Länder'>
<isComposedBy>
<node id='Africa' label='Afrika'>
<isComposedBy>
<node id='DZA' label='Algerien'/>
</isComposedBy>
</node>
</isComposedBy>
</node>
3 changes: 3 additions & 0 deletions dspace-api/src/test/data/dspaceFolder/config/local.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ authority.controlled.dspace.object.owner = true
webui.browse.link.1 = author:dc.contributor.*
webui.browse.link.2 = subject:dc.subject.*

# Configuration required for testing the controlled vocabulary functionality, which is configured using properties
vocabulary.plugin.countries.hierarchy.store=false
vocabulary.plugin.countries.storeIDs=true
# Enable duplicate detection for tests
duplicate.enable = true

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,145 @@ public void testGetMatches() throws IOException, ClassNotFoundException {
assertEquals("north 40", result.values[0].value);
}

/**
* Test of getMatches method of class
* DSpaceControlledVocabulary using a localized controlled vocabulary with no locale (fallback to default)
* @throws java.lang.ClassNotFoundException passed through.
*/
@Test
public void testGetMatchesNoLocale() throws ClassNotFoundException {
final String PLUGIN_INTERFACE = "org.dspace.content.authority.ChoiceAuthority";

String idValue = "DZA";
String labelPart = "Alge";
int start = 0;
int limit = 10;
// This "countries" Controlled Vocab is included in TestEnvironment data
// (under /src/test/data/dspaceFolder/) and it should be auto-loaded
// by test configs in /src/test/data/dspaceFolder/config/local.cfg
DSpaceControlledVocabulary instance = (DSpaceControlledVocabulary)
CoreServiceFactory.getInstance().getPluginService().getNamedPlugin(Class.forName(PLUGIN_INTERFACE),
"countries");
assertNotNull(instance);
Choices result = instance.getMatches(labelPart, start, limit, null);
assertEquals(idValue, result.values[0].value);
assertEquals("Algeria", result.values[0].label);
}

/**
* Test of getBestMatch method of class
* DSpaceControlledVocabulary using a localized controlled vocabulary with no locale (fallback to default)
* @throws java.lang.ClassNotFoundException passed through.
*/
@Test
public void testGetBestMatchIdValueNoLocale() throws ClassNotFoundException {
final String PLUGIN_INTERFACE = "org.dspace.content.authority.ChoiceAuthority";

String idValue = "DZA";
// This "countries" Controlled Vocab is included in TestEnvironment data
// (under /src/test/data/dspaceFolder/) and it should be auto-loaded
// by test configs in /src/test/data/dspaceFolder/config/local.cfg
DSpaceControlledVocabulary instance = (DSpaceControlledVocabulary)
CoreServiceFactory.getInstance().getPluginService().getNamedPlugin(Class.forName(PLUGIN_INTERFACE),
"countries");
assertNotNull(instance);
Choices result = instance.getBestMatch(idValue, null);
assertEquals(idValue, result.values[0].value);
assertEquals("Algeria", result.values[0].label);
}

/**
* Test of getMatches method of class
* DSpaceControlledVocabulary using a localized controlled vocabulary with valid locale parameter (localized
* label returned)
*/
@Test
public void testGetMatchesGermanLocale() throws ClassNotFoundException {
final String PLUGIN_INTERFACE = "org.dspace.content.authority.ChoiceAuthority";

String idValue = "DZA";
String labelPart = "Alge";
int start = 0;
int limit = 10;
// This "countries" Controlled Vocab is included in TestEnvironment data
// (under /src/test/data/dspaceFolder/) and it should be auto-loaded
// by test configs in /src/test/data/dspaceFolder/config/local.cfg
DSpaceControlledVocabulary instance = (DSpaceControlledVocabulary)
CoreServiceFactory.getInstance().getPluginService().getNamedPlugin(Class.forName(PLUGIN_INTERFACE),
"countries");
assertNotNull(instance);
Choices result = instance.getMatches(labelPart, start, limit, "de");
assertEquals(idValue, result.values[0].value);
assertEquals("Algerien", result.values[0].label);
}

/**
* Test of getBestMatch method of class
* DSpaceControlledVocabulary using a localized controlled vocabulary with valid locale parameter (localized
* label returned)
*/
@Test
public void testGetBestMatchIdValueGermanLocale() throws ClassNotFoundException {
final String PLUGIN_INTERFACE = "org.dspace.content.authority.ChoiceAuthority";

String idValue = "DZA";
// This "countries" Controlled Vocab is included in TestEnvironment data
// (under /src/test/data/dspaceFolder/) and it should be auto-loaded
// by test configs in /src/test/data/dspaceFolder/config/local.cfg
DSpaceControlledVocabulary instance = (DSpaceControlledVocabulary)
CoreServiceFactory.getInstance().getPluginService().getNamedPlugin(Class.forName(PLUGIN_INTERFACE),
"countries");
assertNotNull(instance);
Choices result = instance.getBestMatch(idValue, "de");
assertEquals(idValue, result.values[0].value);
assertEquals("Algerien", result.values[0].label);
}

/**
* Test of getChoice method of class
* DSpaceControlledVocabulary using a localized controlled vocabulary with no locale (fallback to default)
* @throws java.lang.ClassNotFoundException passed through.
*/
@Test
public void testGetChoiceNoLocale() throws ClassNotFoundException {
final String PLUGIN_INTERFACE = "org.dspace.content.authority.ChoiceAuthority";

String idValue = "DZA";
// This "countries" Controlled Vocab is included in TestEnvironment data
// (under /src/test/data/dspaceFolder/) and it should be auto-loaded
// by test configs in /src/test/data/dspaceFolder/config/local.cfg
DSpaceControlledVocabulary instance = (DSpaceControlledVocabulary)
CoreServiceFactory.getInstance().getPluginService().getNamedPlugin(Class.forName(PLUGIN_INTERFACE),
"countries");
assertNotNull(instance);
Choice result = instance.getChoice(idValue, null);
assertEquals(idValue, result.value);
assertEquals("Algeria", result.label);
}

/**
* Test of getChoice method of class
* DSpaceControlledVocabulary using a localized controlled vocabulary with valid locale parameter (localized
* label returned)
* @throws java.lang.ClassNotFoundException passed through.
*/
@Test
public void testGetChoiceGermanLocale() throws ClassNotFoundException {
final String PLUGIN_INTERFACE = "org.dspace.content.authority.ChoiceAuthority";

String idValue = "DZA";
// This "countries" Controlled Vocab is included in TestEnvironment data
// (under /src/test/data/dspaceFolder/) and it should be auto-loaded
// by test configs in /src/test/data/dspaceFolder/config/local.cfg
DSpaceControlledVocabulary instance = (DSpaceControlledVocabulary)
CoreServiceFactory.getInstance().getPluginService().getNamedPlugin(Class.forName(PLUGIN_INTERFACE),
"countries");
assertNotNull(instance);
Choice result = instance.getChoice(idValue, "de");
assertEquals(idValue, result.value);
assertEquals("Algerien", result.label);
}

/**
* Test of getBestMatch method, of class DSpaceControlledVocabulary.
*/
Expand Down

0 comments on commit be92570

Please sign in to comment.