| /* |
| * 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 + "]"; |
| } |
| } |