blob: f1abb04314665af5bfbdff938028a9aa2059b2d5 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.sling.i18n.impl;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
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.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceMetadata;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class JcrResourceBundle extends ResourceBundle {
private static final Logger log = LoggerFactory.getLogger(JcrResourceBundle.class);
/** default primary type (=resource type) for message entry dictionaries */
static final String RT_MESSAGE_ENTRY = "sling:MessageEntry";
static final String MIXIN_MESSAGE = "sling:Message";
static final String MIXIN_LANGUAGE = "mix:language";
static final String PROP_KEY = "sling:key";
static final String PROP_VALUE = "sling:message";
static final String PROP_BASENAME = "sling:basename";
static final String PROP_LANGUAGE = "jcr:language";
static final String PROP_MIXINS = "jcr:mixinTypes";
static final String QUERY_LANGUAGE_ROOTS = "//element(*,mix:language)[@jcr:language]";
private final Map<String, Object> resources;
private final Locale locale;
private final String baseName;
private final Set<String> languageRoots = new HashSet<String>();
JcrResourceBundle(Locale locale, String baseName,
ResourceResolver resourceResolver) {
this.locale = locale;
this.baseName = baseName;
log.info("Finding all dictionaries for '{}' (basename: {}) ...", locale, baseName == null ? "<none>" : baseName);
long start = System.currentTimeMillis();
resourceResolver.refresh();
Set<String> roots = loadPotentialLanguageRoots(resourceResolver, locale, baseName);
this.resources = loadFully(resourceResolver, roots, this.languageRoots);
long end = System.currentTimeMillis();
if (log.isInfoEnabled()) {
log.info(
"Finished loading {} entries for '{}' (basename: {}) in {}ms",
new Object[] { resources.size(), locale, baseName == null ? "<none>" : baseName, (end - start)}
);
}
}
protected Set<String> getLanguageRootPaths() {
return languageRoots;
}
@Override
protected void setParent(ResourceBundle parent) {
super.setParent(parent);
}
public ResourceBundle getParent() {
return parent;
}
@Override
public Locale getLocale() {
return locale;
}
public String getBaseName() {
return baseName;
}
/**
* Returns a Set of all resource keys provided by this resource bundle only.
* <p>
* This method is a new Java 1.6 method to implement the
* ResourceBundle.keySet() method.
*
* @return The keys of the resources provided by this resource bundle
*/
@Override
protected Set<String> handleKeySet() {
return resources.keySet();
}
@Override
public Enumeration<String> getKeys() {
Enumeration<String> parentKeys = (parent != null)
? parent.getKeys()
: null;
return new ResourceBundleEnumeration(resources.keySet(), parentKeys);
}
@Override
protected Object handleGetObject(String key) {
if (log.isDebugEnabled()) {
log.debug("Requesting key '{}' from resource bundle (baseName '{}', locale '{}')", new Object[] {key, baseName, locale});
}
return resources.get(key);
}
/**
* Fully loads the resource bundle from the storage.
* <p>
* This method adds entries to the {@code languageRoots} set of strings.
* Therefore this method must not be called concurrently or the set
* must either be thread safe.
*
* @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 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}.
* @return
*
* @throws NullPointerException if either of the parameters is {@code null}.
*/
private Map<String, Object> loadFully(final ResourceResolver resolver, Set<String> roots, Set<String> languageRoots) {
final String[] searchPath = resolver.getSearchPath();
// for each search path entry, have a list of maps (dictionaries)
// plus other = "outside the search path" at the end
// [0] /apps2 -> [dict1, dict2, dict3 ...]
// [1] /apps -> [dict4, dict5, ...]
// [2] /libs -> [dict6, ...]
// [3] (other) -> [dict7, dict8 ...]
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;
}
// linked hash map to keep order (not functionally important, but helpful for dictionary debugging)
Map<String, Object> dictionary = new LinkedHashMap<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;
}
}
targetList.add(dictionary);
// check type of dictionary
if (dictionaryResource.getName().endsWith(".json")) {
loadJsonDictionary(dictionaryResource, dictionary);
} else {
loadSlingMessageDictionary(dictionaryResource, dictionary);
}
languageRoots.add(root);
}
// 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 {}
});
final InputStream stream = resource.adaptTo(InputStream.class);
if (stream != null) {
String encoding = "utf-8";
final ResourceMetadata metadata = resource.getResourceMetadata();
if (metadata.getCharacterEncoding() != null) {
encoding = metadata.getCharacterEncoding();
}
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());
}
}
/**
* Depth-first traversal of a resource tree
*/
private void scanForSlingMessages(final Resource rsrc, final Map<String, Object> targetDictionary) {
final ValueMap vm = rsrc.adaptTo(ValueMap.class);
if ( vm != null ) {
final String value = vm.get(PROP_VALUE, String.class);
if ( value != null ) {
final String key = vm.get(PROP_KEY, rsrc.getName());
targetDictionary.put(key, value);
}
}
for(final Resource c : rsrc.getChildren()) {
scanForSlingMessages(c, targetDictionary);
}
}
private void loadSlingMessageDictionary(final Resource dictionaryResource, final Map<String, Object> targetDictionary) {
log.info("Loading sling:Message dictionary: {}", dictionaryResource.getPath());
this.scanForSlingMessages(dictionaryResource, targetDictionary);
}
private Set<String> loadPotentialLanguageRoots(ResourceResolver resourceResolver, Locale locale, String baseName) {
final String localeString = locale.toString();
final String localeStringLower = localeString.toLowerCase();
final String localeRFC4646String = toRFC4646String(locale);
final String localeRFC4646StringLower = localeRFC4646String.toLowerCase();
final Set<String> paths = new LinkedHashSet<String>();
final Iterator<Resource> bundles = resourceResolver.findResources(QUERY_LANGUAGE_ROOTS, "xpath");
while (bundles.hasNext()) {
Resource bundle = bundles.next();
ValueMap properties = bundle.adaptTo(ValueMap.class);
String language = properties.get(PROP_LANGUAGE, String.class);
if (language != null && language.length() > 0) {
if (language.equals(localeString)
|| language.equals(localeStringLower)
|| language.equals(localeRFC4646String)
|| language.equals(localeRFC4646StringLower)) {
// basename might be a multivalue (see https://issues.apache.org/jira/browse/SLING-4547)
String[] baseNames = properties.get(PROP_BASENAME, new String[]{});
if (baseName == null || Arrays.asList(baseName).contains(baseName)) {
paths.add(bundle.getPath());
}
}
}
}
return Collections.unmodifiableSet(paths);
}
// Would be nice if Locale.toString() output RFC 4646, but it doesn't
private static String toRFC4646String(Locale locale) {
return locale.toString().replace('_', '-');
}
@Override
public String toString() {
return "JcrResourceBundle [locale=" + locale + ", baseName=" + baseName + ", languageRoots=" + languageRoots
+ ", parent=" + parent + "]";
}
}