diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/UrlBuilder.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/UrlBuilder.java index fa874eec0..f43113d8a 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/UrlBuilder.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/UrlBuilder.java @@ -5,7 +5,8 @@ package edu.cornell.mannlib.vitro.webapp.controller.freemarker; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; -import java.util.HashMap; +//import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -129,7 +130,7 @@ public class UrlBuilder { return getUrl(Route.LOGOUT); } - public static class ParamMap extends HashMap { + public static class ParamMap extends LinkedHashMap { private static final long serialVersionUID = 1L; public ParamMap() { } @@ -276,7 +277,7 @@ public class UrlBuilder { } if (profileUrl != null) { - HashMap specialParams = getModelParams(vreq); + LinkedHashMap specialParams = getModelParams(vreq); if(specialParams.size() != 0) { profileUrl = addParams(profileUrl, new ParamMap(specialParams)); } @@ -325,9 +326,9 @@ public class UrlBuilder { //To be used in different property templates so placing method for reuse here //Check if special params included, specifically for menu management and other models - public static HashMap getModelParams(VitroRequest vreq) { + public static LinkedHashMap getModelParams(VitroRequest vreq) { - HashMap specialParams = new HashMap(); + LinkedHashMap specialParams = new LinkedHashMap(); if(vreq != null) { //this parameter is sufficient to switch to menu model String useMenuModelParam = vreq.getParameter(DisplayVocabulary.SWITCH_TO_DISPLAY_MODEL); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java index 5a6e72bd8..3bc2d6045 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/I18n.java @@ -84,7 +84,8 @@ public class I18n { /** * Get an I18nBundle by this name. The request provides the preferred - * Locale, the theme directory and the development mode flag. + * Locale, the application directory, the theme directory and the + * development mode flag. * * If the request indicates that the system is in development mode, then the * cache is cleared on each request. @@ -155,7 +156,8 @@ public class I18n { // ---------------------------------------------------------------------- /** - * Instead of looking in the classpath, look in the theme directory. + * Instead of looking in the classpath, look in the theme i18n directory and + * the application i18n directory. */ private static class ThemeBasedControl extends ResourceBundle.Control { private static final String BUNDLE_DIRECTORY = "i18n/"; @@ -177,7 +179,8 @@ public class I18n { /** * Don't look in the class path, look in the current servlet context, in - * the bundle directory under the theme directory. + * the bundle directory under the theme directory and in the bundle + * directory under the application directory. */ @Override public ResourceBundle newBundle(String baseName, Locale locale, @@ -193,7 +196,7 @@ public class I18n { if (bundleName == null) { throw new NullPointerException("bundleName may not be null."); } - + String themeI18nPath = "/" + themeDirectory + BUNDLE_DIRECTORY; String appI18nPath = "/" + BUNDLE_DIRECTORY; diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/LocaleSelectionFilter.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/LocaleSelectionFilter.java index 377638977..233dad10a 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/LocaleSelectionFilter.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/i18n/selection/LocaleSelectionFilter.java @@ -72,9 +72,9 @@ public class LocaleSelectionFilter implements Filter { */ private static class LocaleSelectionRequestWrapper extends HttpServletRequestWrapper { - private final HttpServletRequest request; - private final Locale selectedLocale; + private final List locales; + @SuppressWarnings("unchecked") public LocaleSelectionRequestWrapper(HttpServletRequest request, Locale selectedLocale) { super(request); @@ -82,31 +82,32 @@ public class LocaleSelectionFilter implements Filter { if (request == null) { throw new NullPointerException("request may not be null."); } - this.request = request; - if (selectedLocale == null) { throw new NullPointerException( "selectedLocale may not be null."); } - this.selectedLocale = selectedLocale; + + Locale selectedLanguage = new Locale(selectedLocale.getLanguage()); + + locales = EnumerationUtils.toList(request.getLocales()); + locales.remove(selectedLanguage); + locales.add(0, selectedLanguage); + locales.remove(selectedLocale); + locales.add(0, selectedLocale); } @Override public Locale getLocale() { - return selectedLocale; + return locales.get(0); } /** - * Put the selected Locale on the front of the list of acceptable - * Locales. + * Get the modified list of locales. */ - @SuppressWarnings({ "unchecked", "rawtypes" }) + @SuppressWarnings("rawtypes") @Override public Enumeration getLocales() { - List list = EnumerationUtils.toList(request.getLocales()); - list.remove(selectedLocale); - list.add(0, selectedLocale); - return Collections.enumeration(list); + return Collections.enumeration(locales); } } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/filter/LanguageFilteringRDFService.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/filter/LanguageFilteringRDFService.java index 725cd1ffe..1df269857 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/filter/LanguageFilteringRDFService.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/filter/LanguageFilteringRDFService.java @@ -11,7 +11,6 @@ import java.util.Comparator; import java.util.Iterator; import java.util.List; -import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -42,26 +41,20 @@ public class LanguageFilteringRDFService implements RDFService { this.langs = normalizeLangs(langs); } - private List normalizeLangs(List langs) { - List normalizedLangs = new ArrayList(); - String currentBaseLang = null; - for (String lang : langs) { - String normalizedLang = StringUtils.lowerCase(lang); - String baseLang = normalizedLang.split("-")[0]; - if (currentBaseLang == null) { - currentBaseLang = baseLang; - } else if (!currentBaseLang.equals(baseLang)) { - if (!normalizedLangs.contains(currentBaseLang)) { - normalizedLangs.add(currentBaseLang); - } - currentBaseLang = baseLang; - } - } - if (currentBaseLang != null && !normalizedLangs.contains(currentBaseLang)) { - normalizedLangs.add(currentBaseLang); - } - return normalizedLangs; - } + private List normalizeLangs(List langs) { + log.debug("Preferred languages:" + langs); + + List normalizedLangs = new ArrayList(langs); + for (String lang : langs) { + String baseLang = lang.split("-")[0]; + if (!normalizedLangs.contains(baseLang)) { + normalizedLangs.add(baseLang); + } + } + + log.debug("Normalized languages:" + normalizedLangs); + return normalizedLangs; + } @Override public boolean changeSetUpdate(ChangeSet changeSet) @@ -106,6 +99,7 @@ public class LanguageFilteringRDFService implements RDFService { } private Model filterModel(Model m) { + log.debug("filterModel"); List retractions = new ArrayList(); StmtIterator stmtIt = m.listStatements(); while (stmtIt.hasNext()) { @@ -117,6 +111,7 @@ public class LanguageFilteringRDFService implements RDFService { continue; } Collections.sort(candidatesForRemoval, new StatementSortByLang()); + log.debug("sorted statements: " + showSortedStatements(candidatesForRemoval)); Iterator candIt = candidatesForRemoval.iterator(); String langRegister = null; boolean chuckRemaining = false; @@ -142,9 +137,27 @@ public class LanguageFilteringRDFService implements RDFService { return m; } - @Override + private String showSortedStatements(List candidatesForRemoval) { + List langStrings = new ArrayList(); + for (Statement stmt: candidatesForRemoval) { + if (stmt == null) { + langStrings.add("null stmt"); + } else { + RDFNode node = stmt.getObject(); + if (!node.isLiteral()) { + langStrings.add("not literal"); + } else { + langStrings.add(node.asLiteral().getLanguage()); + } + } + } + return langStrings.toString(); + } + + @Override public InputStream sparqlSelectQuery(String query, ResultFormat resultFormat) throws RDFServiceException { + log.debug("sparqlSelectQuery: " + query.replaceAll("\\s+", " ")); ResultSet resultSet = ResultSetFactory.fromJSON( s.sparqlSelectQuery(query, RDFService.ResultFormat.JSON)); List solnList = getSolutionList(resultSet); @@ -178,6 +191,7 @@ public class LanguageFilteringRDFService implements RDFService { continue; } Collections.sort(candidatesForRemoval, new RowIndexedLiteralSortByLang()); + log.debug("sorted RowIndexedLiterals: " + showSortedRILs(candidatesForRemoval)); Iterator candIt = candidatesForRemoval.iterator(); String langRegister = null; boolean chuckRemaining = false; @@ -223,7 +237,15 @@ public class LanguageFilteringRDFService implements RDFService { return new ByteArrayInputStream(outputStream.toByteArray()); } - private class RowIndexedLiteral { + private String showSortedRILs(List candidatesForRemoval) { + List langstrings = new ArrayList(); + for (RowIndexedLiteral ril: candidatesForRemoval) { + langstrings.add(ril.getLiteral().getLanguage()); + } + return langstrings.toString(); + } + + private class RowIndexedLiteral { private Literal literal; private int index; @@ -324,37 +346,44 @@ public class LanguageFilteringRDFService implements RDFService { } private class LangSort { + // any inexact match is worse than any exact match + private int inexactMatchPenalty = langs.size(); + // no language is worse than any inexact match (if the preferred list does not include ""). + private int noLanguage = 2 * inexactMatchPenalty; + // no match is worse than no language. + private int noMatch = noLanguage + 1; protected int compareLangs(String t1lang, String t2lang) { - t1lang = StringUtils.lowerCase(t1lang); - t2lang = StringUtils.lowerCase(t2lang); - if ( t1lang == null && t2lang == null) { - return 0; - } else if (t1lang == null) { - return 1; - } else if (t2lang == null) { - return -1; - } else { - int t1langPref = langs.indexOf(t1lang); - int t2langPref = langs.indexOf(t2lang); - if (t1langPref == -1 && t2langPref == -1) { - if ("".equals(t1lang) && "".equals(t2lang)) { - return 0; - } else if ("".equals(t1lang) && !("".equals(t2lang))) { - return -1; - } else { - return 1; - } - } else if (t1langPref > -1 && t2langPref == -1) { - return -1; - } else if (t1langPref == -1 && t2langPref > -1) { - return 1; - } else { - return t1langPref - t2langPref; - } - } + return languageIndex(t1lang) - languageIndex(t2lang); } + /** + * Return index of exact match, or index of partial match, or + * language-free, or no match. + */ + private int languageIndex(String lang) { + if (lang == null) { + lang = ""; + } + + int index = langs.indexOf(lang); + if (index >= 0) { + return index; + } + + if (lang.length() > 2) { + index = langs.indexOf(lang.substring(0, 2)); + if (index >= 0) { + return index + inexactMatchPenalty; + } + } + + if (lang.isEmpty()) { + return noLanguage; + } + + return noMatch; + } } private class RowIndexedLiteralSortByLang extends LangSort implements Comparator { diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/WebXmlTest.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/WebXmlTest.java new file mode 100644 index 000000000..7dc6d1efe --- /dev/null +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/WebXmlTest.java @@ -0,0 +1,308 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp; + +import static org.junit.Assert.fail; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import javax.servlet.Filter; +import javax.servlet.ServletContextListener; +import javax.servlet.http.HttpServlet; +import javax.xml.namespace.NamespaceContext; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.filefilter.IOFileFilter; +import org.apache.commons.io.filefilter.NameFileFilter; +import org.apache.commons.io.filefilter.NotFileFilter; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; + +/** + * Check to see that web.xml doesn't include any constructs that are permitted + * by Tomcat but prohibited by the Servlet 2.4 Specification. + * + * These are things that might not be noticed when testing Vitro on Tomcat, but + * might show up as problems on other containers like GlassFish or WebLogic. + *
    + *
  • + * The contents of elements must be upper-case. Tomcat permits + * lower-case, but the specification does not.
  • + *
  • + * All tags must point to existing classes. Tomcat does not try + * to load a servlet until it is necessary to service a request, but WebLogic + * loads them on startup. Either method is permitted by the specification.
  • + *
+ * + * As long as we're here, let's check some things that would cause Vitro to fail + * in any servlet container. + *
    + *
  • + * All tags must point to existing classes.
  • + *
  • + * All tags must point to existing classes.
  • + *
  • + * All tags must point to existing files.
  • + *
+ */ +@RunWith(value = Parameterized.class) +public class WebXmlTest extends AbstractTestClass { + private static final Log log = LogFactory.getLog(WebXmlTest.class); + + @Parameters + public static Collection findWebXmlFiles() { + IOFileFilter fileFilter = new NameFileFilter("web.xml"); + IOFileFilter dirFilter = new NotFileFilter(new NameFileFilter(".build")); + Collection files = FileUtils.listFiles(new File("."), fileFilter, + dirFilter); + if (files.isEmpty()) { + System.out.println("WARNING: could not find web.xml"); + } else { + if (files.size() > 1) { + System.out + .println("WARNING: testing more than one web.xml file: " + + files); + } + } + + Collection parameters = new ArrayList(); + for (File file : files) { + parameters.add(new Object[] { file }); + } + return parameters; + } + + private static DocumentBuilder docBuilder = createDocBuilder(); + private static XPath xpath = createXPath(); + + private static DocumentBuilder createDocBuilder() { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory + .newInstance(); + factory.setNamespaceAware(true); // never forget this! + return factory.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + throw new RuntimeException(e); + } + } + + private static XPath createXPath() { + XPath xp = XPathFactory.newInstance().newXPath(); + xp.setNamespaceContext(new StupidNamespaceContext()); + return xp; + } + + private File webXmlFile; + private Document webXmlDoc; + private List messages = new ArrayList(); + + public WebXmlTest(File file) { + this.webXmlFile = file; + } + + @Before + public void parseWebXml() throws SAXException, IOException { + if (webXmlDoc == null) { + webXmlDoc = docBuilder.parse(webXmlFile); + } + } + + @Test + public void checkAll() throws IOException { + checkDispatcherValues(); + checkServletClasses(); + checkListenerClasses(); + checkFilterClasses(); + checkTaglibLocations(); + + if (!messages.isEmpty()) { + for (String message : messages) { + System.out.println(message); + } + fail("Found these problems with '" + webXmlFile.getCanonicalPath() + + "'\n " + StringUtils.join(messages, "\n ")); + } + } + + private void checkDispatcherValues() { + List okValues = Arrays.asList(new String[] { "FORWARD", + "REQUEST", "INCLUDE", "ERROR" }); + for (Node n : findNodes("//j2ee:dispatcher")) { + String text = n.getTextContent(); + if (!okValues.contains(text)) { + messages.add("" + text + + " is not valid. Acceptable values are " + + okValues); + } + } + } + + private void checkServletClasses() { + for (Node n : findNodes("//j2ee:servlet-class")) { + String text = n.getTextContent(); + String problem = confirmClassNameIsValid(text, HttpServlet.class); + if (problem != null) { + messages.add("" + text + + " is not valid: " + problem); + } + } + } + + private void checkListenerClasses() { + for (Node n : findNodes("//j2ee:listener-class")) { + String text = n.getTextContent(); + String problem = confirmClassNameIsValid(text, + ServletContextListener.class); + if (problem != null) { + messages.add("" + text + + " is not valid: " + problem); + } + } + } + + private void checkFilterClasses() { + for (Node n : findNodes("//j2ee:filter-class")) { + String text = n.getTextContent(); + String problem = confirmClassNameIsValid(text, Filter.class); + if (problem != null) { + messages.add("" + text + + " is not valid: " + problem); + } + } + } + + private void checkTaglibLocations() { + // TODO Don't know how to do this one. Where do we look for the taglibs? + } + + // ---------------------------------------------------------------------- + // Helper methods + // ---------------------------------------------------------------------- + + /** + * Search for an Xpath, returning a handy list. + */ + private List findNodes(String pattern) { + try { + XPathExpression xpe = xpath.compile(pattern); + NodeList nodes = (NodeList) xpe.evaluate( + webXmlDoc.getDocumentElement(), XPathConstants.NODESET); + List list = new ArrayList(); + for (int i = 0; i < nodes.getLength(); i++) { + list.add(nodes.item(i)); + } + return list; + } catch (XPathExpressionException e) { + throw new RuntimeException(e); + } + } + + /** + * Check that the supplied className can be instantiated with a + * zero-argument constructor, and assigned to a variable of the target + * class. + */ + private String confirmClassNameIsValid(String className, + Class targetClass) { + try { + Class specifiedClass = Class.forName(className); + Object o = specifiedClass.newInstance(); + if (!targetClass.isInstance(o)) { + return specifiedClass.getSimpleName() + + " is not a subclass of " + + targetClass.getSimpleName() + "."; + } + } catch (ClassNotFoundException e) { + return "The class does not exist."; + } catch (InstantiationException e) { + return "The class does not have a public constructor " + + "that takes zero arguments."; + } catch (IllegalAccessException e) { + return "The class does not have a public constructor " + + "that takes zero arguments."; + } + return null; + } + + /** + * Dump the first 20 nodes of an XML context, excluding comments and blank + * text nodes. + */ + @SuppressWarnings("unused") + private int dumpXml(Node xmlNode, int... parms) { + int remaining = (parms.length == 0) ? 20 : parms[0]; + int level = (parms.length < 2) ? 1 : parms[1]; + + Node n = xmlNode; + + if (Node.COMMENT_NODE == n.getNodeType()) { + return 0; + } + if (Node.TEXT_NODE == n.getNodeType()) { + if (StringUtils.isBlank(n.getTextContent())) { + return 0; + } + } + + int used = 1; + + System.out.println(StringUtils.repeat("-->", level) + n); + NodeList nl = n.getChildNodes(); + for (int i = 0; (i < nl.getLength() && remaining > used); i++) { + used += dumpXml(nl.item(i), remaining - used, level + 1); + } + return used; + } + + // ---------------------------------------------------------------------- + // Helper classes + // ---------------------------------------------------------------------- + + private static class StupidNamespaceContext implements NamespaceContext { + @Override + public String getNamespaceURI(String prefix) { + if ("j2ee".equals(prefix)) { + return "http://java.sun.com/xml/ns/j2ee"; + } else { + throw new UnsupportedOperationException(); + } + } + + @Override + public String getPrefix(String namespaceURI) { + throw new UnsupportedOperationException(); + } + + @Override + public Iterator getPrefixes(String namespaceURI) { + throw new UnsupportedOperationException(); + } + } + +} diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/rdfservice/filter/LanguageFilteringRDFServiceTest.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/rdfservice/filter/LanguageFilteringRDFServiceTest.java new file mode 100644 index 000000000..00f4c4c85 --- /dev/null +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/rdfservice/filter/LanguageFilteringRDFServiceTest.java @@ -0,0 +1,309 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.rdfservice.filter; + +import static org.junit.Assert.assertEquals; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.log4j.Level; +import org.junit.Before; +import org.junit.Test; + +import stubs.com.hp.hpl.jena.rdf.model.LiteralStub; + +import com.hp.hpl.jena.rdf.model.Literal; + +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; + +/** + * This is the matching order we expect to see: + * + *
+ * exact match to preferred, by order.
+ * partial match to preferred, by order.
+ * vanilla or null (no language)
+ * no match
+ * 
+ */ +public class LanguageFilteringRDFServiceTest extends AbstractTestClass { + private static final Log log = LogFactory + .getLog(LanguageFilteringRDFServiceTest.class); + + private static final String COLLATOR_CLASSNAME = "edu.cornell.mannlib.vitro.webapp.rdfservice.filter.LanguageFilteringRDFService$RowIndexedLiteralSortByLang"; + private static final String RIL_CLASSNAME = "edu.cornell.mannlib.vitro.webapp.rdfservice.filter.LanguageFilteringRDFService$RowIndexedLiteral"; + + private LanguageFilteringRDFService filteringRDFService; + private List listOfRowIndexedLiterals; + private int literalIndex; + + private List preferredLanguages; + private List availableLanguages; + private List expectedSortOrders; + + @Before + public void setup() { + setLoggerLevel(this.getClass(), Level.DEBUG); + setLoggerLevel(LanguageFilteringRDFService.class, Level.DEBUG); + } + + // ---------------------------------------------------------------------- + // The tests + // ---------------------------------------------------------------------- + + @Test + public void singleMatch() { + preferredLanguages = list("en-US"); + availableLanguages = list("en-US"); + expectedSortOrders = list("en-US"); + testArbitraryOrder(); + } + + @Test + public void singleNoMatch() { + preferredLanguages = list("en-US"); + availableLanguages = list("es-MX"); + expectedSortOrders = list("es-MX"); + testArbitraryOrder(); + } + + @Test + public void doubleMatch() { + preferredLanguages = list("en-US", "es-MX"); + availableLanguages = list("en-US", "es-MX"); + expectedSortOrders = list("en-US", "es-MX"); + testBothWays(); + } + + @Test + public void noMatches() { + preferredLanguages = list("es-MX"); + availableLanguages = list("en-US", "fr-FR"); + expectedSortOrders = list("en-US", "fr-FR"); + testArbitraryOrder(); + } + + @Test + public void partialMatches() { + preferredLanguages = list("en", "es"); + availableLanguages = list("en-US", "es-MX"); + expectedSortOrders = list("en-US", "es-MX"); + testBothWays(); + } + + @Test + public void matchIsBetterThanNoMatch() { + preferredLanguages = list("en-US", "es-MX"); + availableLanguages = list("en-US", "fr-FR"); + expectedSortOrders = list("en-US", "fr-FR"); + testBothWays(); + } + + @Test + public void matchIsBetterThanPartialMatch() { + preferredLanguages = list("es-ES", "en-US"); + availableLanguages = list("en-US", "es-MX"); + expectedSortOrders = list("en-US", "es-MX"); + testBothWays(); + } + + @Test + public void exactMatchIsBetterThanPartialMatch() { + preferredLanguages = list("es"); + availableLanguages = list("es", "es-MX"); + expectedSortOrders = list("es", "es-MX"); + testBothWays(); + } + + @Test + public void matchIsBetterThanVanilla() { + preferredLanguages = list("en-US"); + availableLanguages = list("en-US", ""); + expectedSortOrders = list("en-US", ""); + testBothWays(); + } + + @Test + public void partialMatchIsBetterThanVanilla() { + preferredLanguages = list("es-MX"); + availableLanguages = list("es-ES", ""); + expectedSortOrders = list("es-ES", ""); + testBothWays(); + } + + @Test + public void vanillaIsBetterThanNoMatch() { + preferredLanguages = list("es-MX"); + availableLanguages = list("en-US", ""); + expectedSortOrders = list("", "en-US"); + testBothWays(); + } + + @Test + public void omnibus() { + preferredLanguages = list("es-MX", "es", "en-UK", "es-PE", "fr"); + availableLanguages = list("es-MX", "es", "fr", "es-ES", "fr-FR", "", + "de-DE"); + expectedSortOrders = list("es-MX", "es", "fr", "es-ES", "fr-FR", "", + "de-DE"); + testBothWays(); + } + + // ---------------------------------------------------------------------- + // Helper methods + // ---------------------------------------------------------------------- + + /** + * Sort the available languages as they are presented. Then reverse them and + * sort again. + */ + private void testBothWays() { + createLanguageFilter(); + + buildListOfLiterals(); + sortListOfLiterals(); + assertLanguageOrder("sort literals"); + + buildReversedListOfLiterals(); + sortListOfLiterals(); + assertLanguageOrder("sort reversed literals"); + } + + /** + * Sort the available languages, without caring what the eventual sorted + * order is. Really, this is just a test to see that no exceptions are + * thrown, and no languages are "lost in translation". + */ + private void testArbitraryOrder() { + createLanguageFilter(); + + buildListOfLiterals(); + sortListOfLiterals(); + assertLanguages("sort literals"); + + buildReversedListOfLiterals(); + sortListOfLiterals(); + assertLanguages("sort reversed literals"); + + } + + private List list(String... strings) { + return new ArrayList(Arrays.asList(strings)); + } + + private void createLanguageFilter() { + filteringRDFService = new LanguageFilteringRDFService(null, + preferredLanguages); + } + + private void buildListOfLiterals() { + List list = new ArrayList(); + for (String language : availableLanguages) { + list.add(buildRowIndexedLiteral(language)); + } + listOfRowIndexedLiterals = list; + } + + private void buildReversedListOfLiterals() { + List list = new ArrayList(); + for (String language : availableLanguages) { + list.add(0, buildRowIndexedLiteral(language)); + } + listOfRowIndexedLiterals = list; + } + + private void sortListOfLiterals() { + log.debug("before sorting: " + + languagesFromLiterals(listOfRowIndexedLiterals)); + Comparator comparator = buildRowIndexedLiteralSortByLang(); + Collections.sort(listOfRowIndexedLiterals, comparator); + } + + private void assertLanguageOrder(String message) { + List expectedLanguages = expectedSortOrders; + log.debug("expected order: " + expectedLanguages); + + List actualLanguages = languagesFromLiterals(listOfRowIndexedLiterals); + log.debug("actual order: " + actualLanguages); + + assertEquals(message, expectedLanguages, actualLanguages); + } + + private void assertLanguages(String message) { + Set expectedLanguages = new HashSet(expectedSortOrders); + log.debug("expected languages: " + expectedLanguages); + + Set actualLanguages = new HashSet( + languagesFromLiterals(listOfRowIndexedLiterals)); + log.debug("actual languages: " + actualLanguages); + + assertEquals(message, expectedLanguages, actualLanguages); + } + + private List languagesFromLiterals(List literals) { + List actualLanguages = new ArrayList(); + for (Object ril : literals) { + actualLanguages.add(getLanguageFromRowIndexedLiteral(ril)); + } + return actualLanguages; + } + + // ---------------------------------------------------------------------- + // Reflection methods to get around "private" declarations. + // ---------------------------------------------------------------------- + + private Object buildRowIndexedLiteral(String language) { + try { + Class clazz = Class.forName(RIL_CLASSNAME); + Class[] argTypes = { LanguageFilteringRDFService.class, + Literal.class, Integer.TYPE }; + Constructor constructor = clazz.getDeclaredConstructor(argTypes); + constructor.setAccessible(true); + + Literal l = new LiteralStub(language); + int i = literalIndex++; + return constructor.newInstance(filteringRDFService, l, i); + } catch (Exception e) { + throw new RuntimeException( + "Could not create a row-indexed literal", e); + } + } + + @SuppressWarnings("unchecked") + private Comparator buildRowIndexedLiteralSortByLang() { + try { + Class clazz = Class.forName(COLLATOR_CLASSNAME); + Class[] argTypes = { LanguageFilteringRDFService.class }; + Constructor constructor = clazz.getDeclaredConstructor(argTypes); + constructor.setAccessible(true); + + return (Comparator) constructor + .newInstance(filteringRDFService); + } catch (Exception e) { + throw new RuntimeException("Could not create a collator", e); + } + } + + private String getLanguageFromRowIndexedLiteral(Object ril) { + try { + Method m = ril.getClass().getDeclaredMethod("getLiteral"); + m.setAccessible(true); + Literal l = (Literal) m.invoke(ril); + return l.getLanguage(); + } catch (Exception e) { + throw new RuntimeException( + "Could not get the Literal from a RowIndexedLiteral", e); + } + } + +} diff --git a/webapp/test/stubs/com/hp/hpl/jena/rdf/model/LiteralStub.java b/webapp/test/stubs/com/hp/hpl/jena/rdf/model/LiteralStub.java new file mode 100644 index 000000000..ff7cf9c4e --- /dev/null +++ b/webapp/test/stubs/com/hp/hpl/jena/rdf/model/LiteralStub.java @@ -0,0 +1,179 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package stubs.com.hp.hpl.jena.rdf.model; + +import com.hp.hpl.jena.datatypes.RDFDatatype; +import com.hp.hpl.jena.graph.Node; +import com.hp.hpl.jena.rdf.model.Literal; +import com.hp.hpl.jena.rdf.model.Model; +import com.hp.hpl.jena.rdf.model.RDFNode; +import com.hp.hpl.jena.rdf.model.RDFVisitor; +import com.hp.hpl.jena.rdf.model.Resource; + +/** + * Only implemented what I needed so far. The rest is left as an exercise for + * the student. + */ +public class LiteralStub implements Literal { + // ---------------------------------------------------------------------- + // Stub infrastructure + // ---------------------------------------------------------------------- + + final String language; + + public LiteralStub(String language) { + this.language = language; + } + + // ---------------------------------------------------------------------- + // Stub methods + // ---------------------------------------------------------------------- + + @Override + public boolean isLiteral() { + return true; + } + + @Override + public boolean isAnon() { + return false; + } + + @Override + public boolean isResource() { + return false; + } + + @Override + public boolean isURIResource() { + return false; + } + + @Override + public Literal asLiteral() { + return this; + } + + @Override + public Resource asResource() { + throw new ClassCastException(); + } + + @Override + public String getLanguage() { + return language; + } + + // ---------------------------------------------------------------------- + // Un-implemented methods + // ---------------------------------------------------------------------- + + @Override + public T as(Class view) { + throw new RuntimeException("LiteralStub.as() not implemented."); + } + + @Override + public boolean canAs(Class arg0) { + throw new RuntimeException("LiteralStub.canAs() not implemented."); + } + + @Override + public Model getModel() { + throw new RuntimeException("LiteralStub.getModel() not implemented."); + } + + @Override + public Object visitWith(RDFVisitor arg0) { + throw new RuntimeException("LiteralStub.visitWith() not implemented."); + } + + @Override + public Node asNode() { + throw new RuntimeException("LiteralStub.asNode() not implemented."); + } + + @Override + public boolean getBoolean() { + throw new RuntimeException("LiteralStub.getBoolean() not implemented."); + } + + @Override + public byte getByte() { + throw new RuntimeException("LiteralStub.getByte() not implemented."); + } + + @Override + public char getChar() { + throw new RuntimeException("LiteralStub.getChar() not implemented."); + } + + @Override + public RDFDatatype getDatatype() { + throw new RuntimeException("LiteralStub.getDatatype() not implemented."); + } + + @Override + public String getDatatypeURI() { + throw new RuntimeException( + "LiteralStub.getDatatypeURI() not implemented."); + } + + @Override + public double getDouble() { + throw new RuntimeException("LiteralStub.getDouble() not implemented."); + } + + @Override + public float getFloat() { + throw new RuntimeException("LiteralStub.getFloat() not implemented."); + } + + @Override + public int getInt() { + throw new RuntimeException("LiteralStub.getInt() not implemented."); + } + + @Override + public String getLexicalForm() { + throw new RuntimeException( + "LiteralStub.getLexicalForm() not implemented."); + } + + @Override + public long getLong() { + throw new RuntimeException("LiteralStub.getLong() not implemented."); + } + + @Override + public short getShort() { + throw new RuntimeException("LiteralStub.getShort() not implemented."); + } + + @Override + public String getString() { + throw new RuntimeException("LiteralStub.getString() not implemented."); + } + + @Override + public Object getValue() { + throw new RuntimeException("LiteralStub.getValue() not implemented."); + } + + @Override + public Literal inModel(Model arg0) { + throw new RuntimeException("LiteralStub.inModel() not implemented."); + } + + @Override + public boolean isWellFormedXML() { + throw new RuntimeException( + "LiteralStub.isWellFormedXML() not implemented."); + } + + @Override + public boolean sameValueAs(Literal arg0) { + throw new RuntimeException("LiteralStub.sameValueAs() not implemented."); + } + +} diff --git a/webapp/web/WEB-INF/web.xml b/webapp/web/WEB-INF/web.xml index 73ef58a09..8607ea6f9 100644 --- a/webapp/web/WEB-INF/web.xml +++ b/webapp/web/WEB-INF/web.xml @@ -122,8 +122,8 @@ VitroRequestPrep /* - request - forward + REQUEST + FORWARD @@ -133,7 +133,7 @@ PageRoutingFilter /* - request + REQUEST @@ -164,25 +164,6 @@ --> - - - jsp - org.apache.jasper.servlet.JspServlet - - fork - false - - - xpoweredBy - false - - - trimSpaces - true - - 3 - - IndexController edu.cornell.mannlib.vitro.webapp.search.controller.IndexController @@ -201,15 +182,6 @@ /RecomputeInferences - - SDBSetupController - edu.cornell.mannlib.vitro.webapp.controller.freemarker.SDBSetupController - - - SDBSetupController - /sdbsetup - - MenuManagementEdit edu.cornell.mannlib.vitro.webapp.controller.edit.MenuManagementEdit @@ -228,24 +200,6 @@ /ajax/sparqlQuery - - - - fetch - edu.cornell.mannlib.vitro.webapp.QueryServlet - - - AboutController edu.cornell.mannlib.vitro.webapp.controller.freemarker.AboutController @@ -389,15 +343,6 @@ /editRequestAJAX - - FlagUpdateController - edu.cornell.mannlib.vitro.webapp.controller.edit.FlagUpdateController - - - FlagUpdateController - /flagUpdate - - RDFUploadFormController edu.cornell.mannlib.vitro.webapp.controller.jena.RDFUploadFormController @@ -452,24 +397,6 @@ /jenaXmlFileUpload/* - - OwlImportController - edu.cornell.mannlib.vitro.webapp.owl.OwlImportController - - - OwlImportController - /owl - - - - OwlImportServlet - edu.cornell.mannlib.vitro.webapp.owl.ProtegeOwlImportServlet - - - OwlImportServlet - /importOwl - - JenaAdminServlet edu.cornell.mannlib.vitro.webapp.controller.jena.JenaAdminActions @@ -569,16 +496,6 @@ /datapropEdit - - - KeywordEditController - edu.cornell.mannlib.vitro.webapp.controller.edit.KeywordEditController - - - KeywordEditController - /keywordEdit - - OntologyEditController edu.cornell.mannlib.vitro.webapp.controller.edit.OntologyEditController @@ -777,24 +694,6 @@ /admin/wait - - StatementChangeListingController - edu.cornell.mannlib.vitro.webapp.controller.edit.listing.jena.StatementChangeListingController - - - StatementChangeListingController - /statementHistory - - - - WriteOutChangesController - edu.cornell.mannlib.vitro.webapp.controller.edit.listing.jena.WriteOutChangesController - - - WriteOutChangesController - /writeOutChanges - - ListVClassWebappsController edu.cornell.mannlib.vitro.webapp.controller.freemarker.ListVClassWebappsController @@ -957,15 +856,6 @@ /edit/reorder - - AdminController - edu.cornell.mannlib.vitro.webapp.controller.AdminController - - - AdminController - /adminCon - - TermsOfUseController edu.cornell.mannlib.vitro.webapp.controller.freemarker.TermsOfUseController @@ -1105,32 +995,6 @@ /browse - - pubsbyorg - edu.cornell.mannlib.vitro.webapp.controller.vclass.PubsByDepartmentServlet - - workspaceDir - /usr/local/services/vivo/logs - - - - - - coauthors - edu.cornell.mannlib.vitro.webapp.controller.vclass.CoAuthorServlet - - workspaceDir - /usr/local/services/vivo/logs - - - - - - generic_create - edu.cornell.mannlib.vitro.webapp.GenericDBCreate - - - serveFiles edu.cornell.mannlib.vitro.webapp.filestorage.serving.FileServingServlet @@ -1140,21 +1004,6 @@ /file/* - - generic_editprep - edu.cornell.mannlib.vitro.webapp.GenericDBEditPrep - - - - generic_update - edu.cornell.mannlib.vitro.webapp.GenericDBUpdate - - - - generic_delete - edu.cornell.mannlib.vitro.webapp.GenericDBDelete - - SparqlQuery edu.cornell.mannlib.vitro.webapp.controller.SparqlQueryServlet @@ -1165,16 +1014,6 @@ /admin/sparqlquery - - VisualizationController - edu.cornell.mannlib.vitro.webapp.controller.visualization.VisualizationController - - - - VisualizationController - /visualization - - primitiveRdfEdit edu.cornell.mannlib.vitro.webapp.controller.edit.PrimitiveRdfEdit @@ -1194,10 +1033,6 @@ - - fetch - /fetch - mailusers /mailusers @@ -1256,22 +1091,6 @@ coauthors /coauthors - - generic_create - /generic_create - - - generic_editprep - /generic_editprep - - - generic_update - /generic_update - - - generic_delete - /generic_delete -