Author: fmeschbe
Date: Fri Mar 27 14:30:06 2015
New Revision: 1669598
URL: http://svn.apache.org/r1669598
Log:
SLING-4543 Add support for JSON dictionaries
Applying patch by Alex Klimetscheck (thanks alot !)
Modified:
sling/trunk/contrib/extensions/i18n/src/main/java/org/apache/sling/i18n/impl/JcrResourceBundle.java
sling/trunk/contrib/extensions/i18n/src/main/java/org/apache/sling/i18n/impl/JcrResourceBundleProvider.java
sling/trunk/contrib/extensions/i18n/src/test/java/org/apache/sling/i18n/impl/JcrResourceBundleTest.java
Modified:
sling/trunk/contrib/extensions/i18n/src/main/java/org/apache/sling/i18n/impl/JcrResourceBundle.java
URL:
http://svn.apache.org/viewvc/sling/trunk/contrib/extensions/i18n/src/main/java/org/apache/sling/i18n/impl/JcrResourceBundle.java?rev=1669598&r1=1669597&r2=1669598&view=diff
==============================================================================
---
sling/trunk/contrib/extensions/i18n/src/main/java/org/apache/sling/i18n/impl/JcrResourceBundle.java
(original)
+++
sling/trunk/contrib/extensions/i18n/src/main/java/org/apache/sling/i18n/impl/JcrResourceBundle.java
Fri Mar 27 14:30:06 2015
@@ -18,12 +18,14 @@
*/
package org.apache.sling.i18n.impl;
+import java.io.IOException;
+import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
+import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
@@ -31,9 +33,12 @@ import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
+import org.apache.jackrabbit.commons.json.JsonHandler;
+import org.apache.jackrabbit.commons.json.JsonParser;
import org.apache.jackrabbit.util.ISO9075;
import org.apache.sling.api.SlingException;
import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceMetadata;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.api.resource.ValueMap;
@@ -74,17 +79,19 @@ public class JcrResourceBundle extends R
ResourceResolver resourceResolver) {
this.locale = locale;
+ log.info("Finding all dictionaries for '{}' (basename: {}) ...",
locale, baseName == null ? "<none>" : baseName);
+
long start = System.currentTimeMillis();
refreshSession(resourceResolver);
Set<String> roots = loadPotentialLanguageRoots(resourceResolver,
locale, baseName);
this.resources = loadFully(resourceResolver, roots,
this.languageRoots);
+
long end = System.currentTimeMillis();
- if (log.isDebugEnabled()) {
- log.debug(
- "JcrResourceBundle: Fully loaded {} entries for {} (base: {})
in {}ms",
- new Object[] { resources.size(), locale, baseName,
- (end - start) });
- log.debug("JcrResourceBundle: Language roots: {}", languageRoots);
+ if (log.isInfoEnabled()) {
+ log.info(
+ "Finished loading {} entries for '{}' (basename: {}) in {}ms",
+ new Object[] { resources.size(), locale, baseName == null ?
"<none>" : baseName, (end - start)}
+ );
}
}
@@ -139,11 +146,11 @@ public class JcrResourceBundle extends R
* Therefore this method must not be called concurrently or the set
* must either be thread safe.
*
- * @param resourceResolver The storage access (must not be {@code null})
- * @param roots The set of (potential) disctionary subtrees. This must
+ * @param resolver The storage access (must not be {@code null})
+ * @param roots The set of (potential) dictionary subtrees. This must
* not be {@code null}. If empty, no resources will actually be
* loaded.
- * @param languageRoots The set of actualy dictionary subtrees. While
+ * @param languageRoots The set of actually dictionary subtrees. While
* processing the resources, all subtrees listed in the {@code roots}
* set is added to this set if it actually contains resources. This
* must not be {@code null}.
@@ -152,83 +159,169 @@ public class JcrResourceBundle extends R
* @throws NullPointerException if either of the parameters is {@code
null}.
*/
@SuppressWarnings("deprecation")
- private Map<String, Object> loadFully(final ResourceResolver
resourceResolver, Set<String> roots, Set<String> languageRoots) {
- final List<List<Map<String, Object>>> allResources = new
ArrayList<List<Map<String,Object>>>();
+ private Map<String, Object> loadFully(final ResourceResolver resolver,
Set<String> roots, Set<String> languageRoots) {
- final String[] path = resourceResolver.getSearchPath();
+ final String[] searchPath = resolver.getSearchPath();
- for (final String root: roots) {
- String fullLoadQuery = String.format(QUERY_MESSAGES_FORMAT,
ISO9075.encodePath(root));
+ // for each search path entry, have a list of maps (dictionaries)
+ // plus other = "outside the search path" at the end
- log.debug("Executing full load query {}", fullLoadQuery);
+ // [0] /apps2 -> [dict1, dict2, dict3 ...]
+ // [1] /apps -> [dict4, dict5, ...]
+ // [2] /libs -> [dict6, ...]
+ // [3] (other) -> [dict7, dict8 ...]
- // do an XPath query because this won't go away soon and still
- // (2011/04/04) is the fastest query language ...
- Iterator<Map<String, Object>> bundles = null;
- try {
- bundles = resourceResolver.queryResources(fullLoadQuery,
"xpath");
- } catch (final SlingException se) {
- log.error("Exception during resource query " + fullLoadQuery,
se);
+ List<List<Map<String, Object>>> dictionariesBySearchPath = new
ArrayList<List<Map<String, Object>>>(searchPath.length + 1);
+ for (int i = 0; i < searchPath.length + 1; i++) {
+ dictionariesBySearchPath.add(new ArrayList<Map<String, Object>>());
+ }
+
+ for (final String root: roots) {
+
+ Resource dictionaryResource = resolver.getResource(root);
+ if (dictionaryResource == null) {
+ log.warn("Dictionary root found by search not accessible: {}",
root);
+ continue;
}
- if ( bundles != null ) {
+ // linked hash map to keep order (not functionally important, but
helpful for dictionary debugging)
+ Map<String, Object> dictionary = new LinkedHashMap<String,
Object>();
- final Map<String, Object> rest = new HashMap<String, Object>();
- final List<Map<String, Object>> res0 = new
ArrayList<Map<String, Object>>();
- for (int i = 0; i < path.length; i++) {
- res0.add(new HashMap<String, Object>());
+ // find where in the search path this dict belongs
+ // otherwise put it in the outside-the-search-path bucket (last
list)
+ List<Map<String, Object>> targetList =
dictionariesBySearchPath.get(searchPath.length);
+ for (int i = 0; i < searchPath.length; i++) {
+ if (root.startsWith(searchPath[i])) {
+ targetList = dictionariesBySearchPath.get(i);
+ break;
}
- res0.add(rest); // add global list at the end
+ }
+ targetList.add(dictionary);
- allResources.add(res0);
+ // check type of dictionary
+ if (dictionaryResource.getName().endsWith(".json")) {
+ loadJsonDictionary(dictionaryResource, dictionary);
+ } else {
+ loadSlingMessageDictionary(resolver, root, dictionary);
+ }
- while (bundles.hasNext()) {
- final Map<String, Object> row = bundles.next();
- if (row.containsKey(PROP_VALUE)) {
- final String jcrPath = (String) row.get(JCR_PATH);
- String key = (String) row.get(PROP_KEY);
-
- if (key == null) {
- key = ResourceUtil.getName(jcrPath);
- }
-
- Map<String, Object> dst = rest;
- for (int i = 0; i < path.length; i++) {
- if (jcrPath.startsWith(path[i])) {
- dst = res0.get(i);
- break;
- }
- }
+ if (!dictionary.isEmpty()) {
+ languageRoots.add(root);
+ }
+ }
- dst.put(key, row.get(PROP_VALUE));
- }
+ // linked hash map to keep order (not functionally important, but
helpful for dictionary debugging)
+ final Map<String, Object> result = new LinkedHashMap<String, Object>();
+
+ // first, add everything that's not under a search path (e.g. /content)
+ // below, same strings inside a search path dictionary would overlay
them since
+ // they are added later to result = overwrite
+ for (Map<String, Object> dict :
dictionariesBySearchPath.get(searchPath.length)) {
+ result.putAll(dict);
+ }
+
+ // then, in order of the search path, add all the individual
dictionaries into
+ // a single result, so that e.g. strings in /apps overlay the ones in
/libs
+ for (int i = searchPath.length - 1; i >= 0; i--) {
+
+ for (Map<String, Object> dict : dictionariesBySearchPath.get(i)) {
+ result.putAll(dict);
+ }
+ }
+
+ return result;
+ }
+
+ private void loadJsonDictionary(Resource resource, final Map<String,
Object> targetDictionary) {
+ log.info("Loading json dictionary: {}", resource.getPath());
+
+ // use streaming parser (we don't need the dict in memory twice)
+ JsonParser parser = new JsonParser(new JsonHandler() {
+
+ private String key;
+
+ @Override
+ public void key(String key) throws IOException {
+ this.key = key;
+ }
+
+ @Override
+ public void value(String value) throws IOException {
+ targetDictionary.put(key, value);
+ }
+
+ @Override
+ public void object() throws IOException {}
+ @Override
+ public void endObject() throws IOException {}
+ @Override
+ public void array() throws IOException {}
+ @Override
+ public void endArray() throws IOException {}
+ @Override
+ public void value(boolean value) throws IOException {}
+ @Override
+ public void value(long value) throws IOException {}
+ @Override
+ public void value(double value) throws IOException {}
+ });
+
+ InputStream stream = resource.adaptTo(InputStream.class);
+ if (stream != null) {
+ String encoding = "utf-8";
+ ResourceMetadata metadata = resource.getResourceMetadata();
+ if (metadata != null) { // test does not implement metadata
+ if (metadata.getCharacterEncoding() != null) {
+ encoding = metadata.getCharacterEncoding();
}
+ }
- for (int i = res0.size() - 1; i >= 0; i--) {
- final Map<String, Object> resources = res0.get(i);
- if (!resources.isEmpty()) {
- // also remember root (in case we face a non-empty map)
- languageRoots.add(root);
- break;
- }
+ try {
+
+ parser.parse(stream, encoding);
+
+ } catch (IOException e) {
+ log.warn("Could not parse i18n json dictionary {}: {}",
resource.getPath(), e.getMessage());
+ } finally {
+ try {
+ stream.close();
+ } catch (IOException ignore) {
}
}
+ } else {
+ log.warn("Not a json file: {}", resource.getPath());
}
- final Map<String, Object> result = new HashMap<String, Object>();
- for(final List<Map<String, Object>> current : allResources) {
- final Map<String, Object> rest = current.get(current.size() - 1);
- result.putAll(rest);
- }
+ }
+
+ private void loadSlingMessageDictionary(ResourceResolver resourceResolver,
String path, Map<String, Object> targetDictionary) {
+ // run query for sling:Message nodes
+ String dictQuery = String.format(QUERY_MESSAGES_FORMAT,
ISO9075.encodePath(path));
+
+ log.info("Loading sling:Message dictionary: {}", path);
+ log.info("Executing query {}", dictQuery);
+
+ try {
+ // do an XPath query because this won't go away soon and still
+ // (2011/04/04) is the fastest query language ...
+ Iterator<Map<String, Object>> queryResult =
resourceResolver.queryResources(dictQuery, "xpath");
- for (int i = path.length - 1; i >= 0; i--) {
+ while (queryResult.hasNext()) {
+ final Map<String, Object> row = queryResult.next();
+ if (row.containsKey(PROP_VALUE)) {
+ final String jcrPath = (String) row.get(JCR_PATH);
+ String key = (String) row.get(PROP_KEY);
- for(final List<Map<String, Object>> current : allResources) {
- final Map<String, Object> resources = current.get(i);
- result.putAll(resources);
+ if (key == null) {
+ key = ResourceUtil.getName(jcrPath);
+ }
+
+ targetDictionary.put(key, row.get(PROP_VALUE));
+ }
}
- }
- return result;
+ } catch (final SlingException se) {
+ log.error("Exception during resource query " + dictQuery, se);
+ }
}
private Set<String> loadPotentialLanguageRoots(ResourceResolver
resourceResolver, Locale locale, String baseName) {
Modified:
sling/trunk/contrib/extensions/i18n/src/main/java/org/apache/sling/i18n/impl/JcrResourceBundleProvider.java
URL:
http://svn.apache.org/viewvc/sling/trunk/contrib/extensions/i18n/src/main/java/org/apache/sling/i18n/impl/JcrResourceBundleProvider.java?rev=1669598&r1=1669597&r2=1669598&view=diff
==============================================================================
---
sling/trunk/contrib/extensions/i18n/src/main/java/org/apache/sling/i18n/impl/JcrResourceBundleProvider.java
(original)
+++
sling/trunk/contrib/extensions/i18n/src/main/java/org/apache/sling/i18n/impl/JcrResourceBundleProvider.java
Fri Mar 27 14:30:06 2015
@@ -324,6 +324,7 @@ public class JcrResourceBundleProvider i
final Set<String> languageRoots =
resourceBundle.getLanguageRootPaths();
languageRootPaths.addAll(languageRoots);
log.debug("registerResourceBundle({}, ...): added service registration
and language roots {}", key, languageRoots);
+ log.info("Currently loaded dictionaries across all locales: {}",
languageRootPaths);
}
/**
Modified:
sling/trunk/contrib/extensions/i18n/src/test/java/org/apache/sling/i18n/impl/JcrResourceBundleTest.java
URL:
http://svn.apache.org/viewvc/sling/trunk/contrib/extensions/i18n/src/test/java/org/apache/sling/i18n/impl/JcrResourceBundleTest.java?rev=1669598&r1=1669597&r2=1669598&view=diff
==============================================================================
---
sling/trunk/contrib/extensions/i18n/src/test/java/org/apache/sling/i18n/impl/JcrResourceBundleTest.java
(original)
+++
sling/trunk/contrib/extensions/i18n/src/test/java/org/apache/sling/i18n/impl/JcrResourceBundleTest.java
Fri Mar 27 14:30:06 2015
@@ -18,6 +18,8 @@
*/
package org.apache.sling.i18n.impl;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
@@ -25,6 +27,7 @@ import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
+import javax.jcr.Binary;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
@@ -35,6 +38,7 @@ import javax.jcr.query.RowIterator;
import javax.naming.NamingException;
import javax.servlet.http.HttpServletRequest;
+import org.apache.jackrabbit.JcrConstants;
import org.apache.sling.api.resource.AbstractResource;
import org.apache.sling.api.resource.PersistenceException;
import org.apache.sling.api.resource.Resource;
@@ -85,62 +89,7 @@ public class JcrResourceBundleTest exten
@Override
public Resource next() {
final Node node = nodes.nextNode();
- final Resource rsrc = new AbstractResource() {
-
- @Override
- public String getResourceType() {
- // TODO Auto-generated method stub
- return null;
- }
-
- @Override
- public String getResourceSuperType() {
- // TODO Auto-generated method stub
- return null;
- }
-
- @Override
- public ResourceResolver getResourceResolver() {
- // TODO Auto-generated method stub
- return null;
- }
-
- @Override
- public ResourceMetadata getResourceMetadata() {
- // TODO Auto-generated method stub
- return null;
- }
-
- @Override
- public String getPath() {
- try {
- return node.getPath();
- } catch ( final RepositoryException re ) {
- throw new RuntimeException(re);
- }
- }
-
- @Override
- public <AdapterType> AdapterType adaptTo(
- Class<AdapterType> type) {
- if ( type == ValueMap.class) {
- try {
- final Map<String, Object> props =
new HashMap<String, Object>();
- if (
node.hasProperty(JcrResourceBundle.PROP_LANGUAGE) ) {
-
props.put(JcrResourceBundle.PROP_LANGUAGE,
node.getProperty(JcrResourceBundle.PROP_LANGUAGE).getString());
- }
- if (
node.hasProperty(JcrResourceBundle.PROP_BASENAME) ) {
-
props.put(JcrResourceBundle.PROP_BASENAME,
node.getProperty(JcrResourceBundle.PROP_BASENAME).getString());
- }
- return (AdapterType)new
ValueMapDecorator(props);
- } catch ( final RepositoryException re
) {
- throw new RuntimeException(re);
- }
- }
- return super.adaptTo(type);
- }
- };
- return rsrc;
+ return new TestResource(node);
}
@Override
@@ -163,7 +112,17 @@ public class JcrResourceBundleTest exten
@Override
public Resource getResource(String path) {
- // TODO Auto-generated method stub
+ try {
+ Node n = getSession().getNode(path);
+ if (n != null) {
+ return new TestResource(n);
+ }
+
+ } catch (NamingException ne) {
+ //ignore
+ } catch (RepositoryException re) {
+ //ignore
+ }
return null;
}
@@ -550,6 +509,52 @@ public class JcrResourceBundleTest exten
assertEquals(MESSAGES_DE.size(), counter);
}
+ public void test_outside_search_path() throws Exception {
+ Node libsI18n = getSession().getRootNode().getNode("libs/i18n");
+ libsI18n.remove();
+
+ // dict outside search path: /content
+ Node contentI18n =
getSession().getRootNode().addNode("content").addNode("i18n",
"nt:unstructured");
+ Node de = contentI18n.addNode("de", "nt:folder");
+ de.addMixin("mix:language");
+ de.setProperty("jcr:language", "de");
+ for (Message msg : MESSAGES_DE.values()) {
+ msg.add(de);
+ }
+ getSession().save();
+
+ // test if /content dictionary is read at all
+ JcrResourceBundle bundle = new JcrResourceBundle(new Locale("de"),
null, resolver);
+ for (Message msg : MESSAGES_DE.values()) {
+ assertEquals(msg.message, bundle.getString(msg.key));
+ }
+
+ // now overwrite /content dict in /libs
+ libsI18n = getSession().getRootNode().getNode("libs").addNode("i18n",
"nt:unstructured");
+ de = libsI18n.addNode("de", "nt:folder");
+ de.addMixin("mix:language");
+ de.setProperty("jcr:language", "de");
+ for (Message msg : MESSAGES_DE_APPS.values()) {
+ msg.add(de);
+ }
+ getSession().save();
+
+ // test if /libs (something in the search path) overlays /content
(outside the search path)
+ bundle = new JcrResourceBundle(new Locale("de"), null, resolver);
+ for (Message msg : MESSAGES_DE_APPS.values()) {
+ assertEquals(msg.message, bundle.getString(msg.key));
+ }
+
+ // test getKeys
+ Enumeration<String> keys = bundle.getKeys();
+ int counter = 0;
+ while (keys.hasMoreElements()) {
+ counter++;
+ String key = keys.nextElement();
+ assertTrue("bundle returned key that is not supposed to be there:
" + key, MESSAGES_DE_APPS.containsKey(key));
+ }
+ assertEquals(MESSAGES_DE.size(), counter);
+ }
public void test_basename() throws Exception {
// create another de lib with a basename set
@@ -579,4 +584,119 @@ public class JcrResourceBundleTest exten
}
assertEquals(MESSAGES_DE.size(), counter);
}
+
+ public void test_json_dictionary() throws Exception {
+ Node appsI18n =
getSession().getRootNode().addNode("apps").addNode("i18n", "nt:unstructured");
+ Node deJson = appsI18n.addNode("de.json", "nt:file");
+ deJson.addMixin("mix:language");
+ deJson.setProperty("jcr:language", "de");
+ Node content = deJson.addNode("jcr:content", "nt:resource");
+ content.setProperty("jcr:mimeType", "application/json");
+
+ // manually creating json file, good enough for the test
+ StringBuilder json = new StringBuilder();
+ json.append("{");
+ for (Message msg : MESSAGES_DE_APPS.values()) {
+ json.append("\"").append(msg.key).append("\": \"");
+ json.append(msg.message).append("\",\n");
+ }
+ json.append("}");
+
+ InputStream stream = new
ByteArrayInputStream(json.toString().getBytes());
+ Binary binary = getSession().getValueFactory().createBinary(stream);
+ content.setProperty("jcr:data", binary);
+ getSession().save();
+
+ // test getString
+ JcrResourceBundle bundle = new JcrResourceBundle(new Locale("de"),
null, resolver);
+ for (Message msg : MESSAGES_DE_APPS.values()) {
+ assertEquals(msg.message, bundle.getString(msg.key));
+ }
+
+ // test getKeys
+ Enumeration<String> keys = bundle.getKeys();
+ int counter = 0;
+ while (keys.hasMoreElements()) {
+ counter++;
+ String key = keys.nextElement();
+ assertTrue("bundle returned key that is not supposed to be there:
" + key, MESSAGES_DE_APPS.containsKey(key));
+ }
+ assertEquals(MESSAGES_DE.size(), counter);
+ }
+
+ private class TestResource extends AbstractResource {
+
+ private Node node = null;
+ public TestResource(Node xnode) {
+ super();
+ node = xnode;
+ }
+
+ @Override
+ public String getResourceType() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public String getResourceSuperType() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public ResourceResolver getResourceResolver() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public ResourceMetadata getResourceMetadata() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public String getPath() {
+ try {
+ return node.getPath();
+ } catch ( final RepositoryException re ) {
+ throw new RuntimeException(re);
+ }
+ }
+
+ @Override
+ public <AdapterType> AdapterType adaptTo(
+ Class<AdapterType> type) {
+ if ( type == ValueMap.class) {
+ try {
+ final Map<String, Object> props = new HashMap<String,
Object>();
+ if ( node.hasProperty(JcrResourceBundle.PROP_LANGUAGE) ) {
+ props.put(JcrResourceBundle.PROP_LANGUAGE,
node.getProperty(JcrResourceBundle.PROP_LANGUAGE).getString());
+ }
+ if ( node.hasProperty(JcrResourceBundle.PROP_BASENAME) ) {
+ props.put(JcrResourceBundle.PROP_BASENAME,
node.getProperty(JcrResourceBundle.PROP_BASENAME).getString());
+ }
+ return (AdapterType)new ValueMapDecorator(props);
+ } catch ( final RepositoryException re ) {
+ throw new RuntimeException(re);
+ }
+ }
+ if (type == InputStream.class) {
+ try {
+ if (node.hasNode(JcrConstants.JCR_CONTENT)) {
+ return (AdapterType)
node.getNode(JcrConstants.JCR_CONTENT).getProperty(JcrConstants.JCR_DATA).getBinary().getStream();
+ } else {
+ return null;
+ }
+ } catch (RepositoryException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ if (type == Node.class) {
+ return (AdapterType)node;
+ }
+ return super.adaptTo(type);
+ }
+ };
}