/**
* 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.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.io.File;
import org.apache.commons.lang.ArrayUtils;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.xml.sax.InputSource;
import org.apache.log4j.Logger;
import org.dspace.core.ConfigurationManager;
import org.dspace.core.SelfNamedPlugin;
/**
* ChoiceAuthority source that reads the JSPUI-style hierarchical vocabularies
* from ${dspace.dir}/config/controlled-vocabularies/*.xml and turns them into
* autocompleting authorities.
*
* Configuration:
* This MUST be configured as a self-named plugin, e.g.:
* plugin.selfnamed.org.dspace.content.authority.ChoiceAuthority = \
* org.dspace.content.authority.DSpaceControlledVocabulary
*
* It AUTOMATICALLY configures a plugin instance for each XML file in the
* controlled vocabularies directory. The name of the plugin is the basename
* of the file; e.g., "${dspace.dir}/config/controlled-vocabularies/nsi.xml"
* would generate a plugin called "nsi".
*
* Each configured plugin comes with three configuration options:
* vocabulary.plugin._plugin_.hierarchy.store = <true|false> # Store entire hierarchy along with selected value. Default: TRUE
* vocabulary.plugin._plugin_.hierarchy.suggest = <true|false> # Display entire hierarchy in the suggestion list. Default: TRUE
* vocabulary.plugin._plugin_.delimiter = "<string>" # Delimiter to use when building hierarchy strings. Default: "::"
*
*
* @author Michael B. Klein
*
*/
public class DSpaceControlledVocabulary extends SelfNamedPlugin implements ChoiceAuthority
{
private static Logger log = Logger.getLogger(DSpaceControlledVocabulary.class);
private static String xpathTemplate = "//node[contains(translate(@label,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'),'%s')]";
private static String idTemplate = "//node[@id = '%s']";
private static String pluginNames[] = null;
private String vocabularyName = null;
private InputSource vocabulary = null;
private Boolean suggestHierarchy = true;
private Boolean storeHierarchy = true;
private String hierarchyDelimiter = "::";
public DSpaceControlledVocabulary()
{
super();
}
public static String[] getPluginNames()
{
if (pluginNames == null)
{
initPluginNames();
}
return (String[]) ArrayUtils.clone(pluginNames);
}
private static synchronized void initPluginNames()
{
if (pluginNames == null)
{
class xmlFilter implements java.io.FilenameFilter
{
@Override
public boolean accept(File dir, String name)
{
return name.endsWith(".xml");
}
}
String vocabulariesPath = ConfigurationManager.getProperty("dspace.dir")
+ "/config/controlled-vocabularies/";
String[] xmlFiles = (new File(vocabulariesPath)).list(new xmlFilter());
List<String> names = new ArrayList<String>();
for (String filename : xmlFiles)
{
names.add((new File(filename)).getName().replace(".xml", ""));
}
pluginNames = names.toArray(new String[names.size()]);
log.info("Got plugin names = " + Arrays.deepToString(pluginNames));
}
}
private void init()
{
if (vocabulary == null)
{
log.info("Initializing " + this.getClass().getName());
vocabularyName = this.getPluginInstanceName();
String vocabulariesPath = ConfigurationManager.getProperty("dspace.dir") + "/config/controlled-vocabularies/";
String configurationPrefix = "vocabulary.plugin." + vocabularyName;
storeHierarchy = ConfigurationManager.getBooleanProperty(configurationPrefix + ".hierarchy.store", storeHierarchy);
suggestHierarchy = ConfigurationManager.getBooleanProperty(configurationPrefix + ".hierarchy.suggest", suggestHierarchy);
String configuredDelimiter = ConfigurationManager.getProperty(configurationPrefix + ".delimiter");
if (configuredDelimiter != null)
{
hierarchyDelimiter = configuredDelimiter.replaceAll("(^\"|\"$)","");
}
String filename = vocabulariesPath + vocabularyName + ".xml";
log.info("Loading " + filename);
vocabulary = new InputSource(filename);
}
}
private String buildString(Node node)
{
if (node.getNodeType() == Node.DOCUMENT_NODE)
{
return("");
}
else
{
String parentValue = buildString(node.getParentNode());
Node currentLabel = node.getAttributes().getNamedItem("label");
if (currentLabel != null)
{
String currentValue = currentLabel.getNodeValue();
if (parentValue.equals(""))
{
return currentValue;
}
else
{
return(parentValue + this.hierarchyDelimiter + currentValue);
}
}
else
{
return(parentValue);
}
}
}
@Override
public Choices getMatches(String field, String text, int collection, int start, int limit, String locale)
{
init();
log.debug("Getting matches for '" + text + "'");
String xpathExpression = String.format(xpathTemplate, text.replaceAll("'", "'").toLowerCase());
XPath xpath = XPathFactory.newInstance().newXPath();
Choice[] choices;
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()];
for (int i = 0; i < results.getLength(); i++)
{
Node node = results.item(i);
String hierarchy = this.buildString(node);
if (this.suggestHierarchy)
{
labels[i] = hierarchy;
}
else
{
labels[i] = node.getAttributes().getNamedItem("label").getNodeValue();
}
if (this.storeHierarchy)
{
values[i] = hierarchy;
}
else
{
values[i] = node.getAttributes().getNamedItem("label").getNodeValue();
}
Node idAttr = node.getAttributes().getNamedItem("id");
if (null != idAttr) // 'id' is optional
authorities[i] = idAttr.getNodeValue();
}
int resultCount = labels.length - start;
if ((limit > 0) && (resultCount > limit)) // limit = 0 means no 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]);
}
}
} catch(XPathExpressionException e) {
choices = new Choice[0];
}
return new Choices(choices, 0, choices.length, Choices.CF_AMBIGUOUS, false);
}
@Override
public Choices getBestMatch(String field, String text, int collection, 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);
XPath xpath = XPathFactory.newInstance().newXPath();
try {
Node node = (Node)xpath.evaluate(xpathExpression, vocabulary, XPathConstants.NODE);
return node.getAttributes().getNamedItem("label").getNodeValue();
} catch(XPathExpressionException e) {
return("");
}
}
}