| /* |
| * 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 static org.apache.sling.i18n.impl.JcrResourceBundle.PROP_BASENAME; |
| import static org.apache.sling.i18n.impl.JcrResourceBundle.PROP_LANGUAGE; |
| import static org.apache.sling.i18n.impl.JcrResourceBundle.PROP_PATH; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.Dictionary; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Hashtable; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.MissingResourceException; |
| import java.util.ResourceBundle; |
| import java.util.Set; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.CopyOnWriteArrayList; |
| import java.util.concurrent.Semaphore; |
| import java.util.regex.Pattern; |
| |
| import org.apache.sling.api.resource.LoginException; |
| import org.apache.sling.api.resource.Resource; |
| import org.apache.sling.api.resource.ResourceResolver; |
| import org.apache.sling.api.resource.ResourceResolverFactory; |
| import org.apache.sling.api.resource.ValueMap; |
| import org.apache.sling.api.resource.observation.ExternalResourceChangeListener; |
| import org.apache.sling.api.resource.observation.ResourceChange; |
| import org.apache.sling.api.resource.observation.ResourceChangeListener; |
| import org.apache.sling.commons.scheduler.ScheduleOptions; |
| import org.apache.sling.commons.scheduler.Scheduler; |
| import org.apache.sling.i18n.ResourceBundleProvider; |
| import org.apache.sling.serviceusermapping.ServiceUserMapped; |
| import org.osgi.framework.Bundle; |
| import org.osgi.framework.BundleContext; |
| import org.osgi.framework.Constants; |
| import org.osgi.framework.ServiceRegistration; |
| import org.osgi.service.component.annotations.Activate; |
| import org.osgi.service.component.annotations.Component; |
| import org.osgi.service.component.annotations.Deactivate; |
| import org.osgi.service.component.annotations.Reference; |
| import org.osgi.service.metatype.annotations.Designate; |
| import org.osgi.util.tracker.BundleTracker; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * The <code>JcrResourceBundleProvider</code> implements the |
| * <code>ResourceBundleProvider</code> interface creating |
| * <code>ResourceBundle</code> instances from resources stored in the |
| * repository. |
| */ |
| @Component(service = {ResourceBundleProvider.class, ResourceChangeListener.class}, |
| property = { |
| Constants.SERVICE_DESCRIPTION + "=Apache Sling I18n Resource Bundle Provider", |
| Constants.SERVICE_VENDOR + "=The Apache Software Foundation", |
| ResourceChangeListener.PATHS + "=/", |
| ResourceChangeListener.CHANGES + "=ADDED", |
| ResourceChangeListener.CHANGES + "=REMOVED", |
| ResourceChangeListener.CHANGES + "=CHANGED" |
| }) |
| @Designate(ocd = Config.class) |
| public class JcrResourceBundleProvider implements ResourceBundleProvider, ResourceChangeListener, ExternalResourceChangeListener { |
| |
| /** default log */ |
| private final Logger log = LoggerFactory.getLogger(getClass()); |
| |
| /** |
| * A regular expression pattern matching all custom country codes. |
| * @see <a href="https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#User-assigned_code_elements">User-assigned code elements</a> |
| */ |
| private static final Pattern USER_ASSIGNED_COUNTRY_CODES_PATTERN = Pattern.compile("aa|q[m-z]|x[a-z]|zz"); |
| |
| @Reference |
| private Scheduler scheduler; |
| |
| /** job names of scheduled jobs for reloading individual bundles */ |
| private final Collection<String> scheduledJobNames = Collections.synchronizedList(new ArrayList<String>()) ; |
| |
| @Reference |
| private ResourceResolverFactory resourceResolverFactory; |
| |
| @Reference |
| private ServiceUserMapped serviceUserMapped; |
| |
| /** |
| * The default Locale as configured with the <i>locale.default</i> |
| * configuration property. This defaults to <code>Locale.ENGLISH</code> if |
| * the configuration property is not set. |
| */ |
| private volatile Locale defaultLocale = Locale.ENGLISH; |
| |
| /** |
| * Map of cached resource bundles indexed by a key combined of the base name |
| * and <code>Locale</code> used to load and identify the <code>ResourceBundle</code>. |
| */ |
| private final ConcurrentHashMap<Key, JcrResourceBundle> resourceBundleCache = new ConcurrentHashMap<>(); |
| |
| private final ConcurrentHashMap<Key, Semaphore> loadingGuards = new ConcurrentHashMap<>(); |
| |
| /** |
| * paths from which JCR resource bundles have been loaded |
| */ |
| private final Set<String> languageRootPaths = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>()); |
| |
| /** |
| * Return root resource bundle as created on-demand by |
| * {@link #getRootResourceBundle()}. |
| */ |
| private volatile ResourceBundle rootResourceBundle; |
| |
| private volatile BundleContext bundleContext; |
| |
| /** |
| * Each ResourceBundle is registered as a service. Each registration is stored in this map with the locale & base name used as a key. |
| */ |
| private final Map<Key, ServiceRegistration<ResourceBundle>> bundleServiceRegistrations = new HashMap<>(); |
| |
| private BundleTracker<Set<LocatorPaths>> locatorPathsTracker; |
| private List<LocatorPaths> locatorPaths = new CopyOnWriteArrayList<>(); |
| |
| /** |
| * Filter to check for allowed paths |
| */ |
| private volatile PathFilter pathFilter; |
| |
| private volatile boolean preloadBundles; |
| |
| private volatile long invalidationDelay; |
| |
| /** |
| * Add a set of paths to the set that are inspected to |
| * look for resource bundle resources |
| * |
| * @param locatorPathsSet set of locator paths to check |
| */ |
| public void registerLocatorPaths(Set<LocatorPaths> locatorPathsSet) { |
| this.locatorPaths.addAll(locatorPathsSet); |
| clearCache(); |
| } |
| |
| /** |
| * Remove a set of paths from the set that are inspected to |
| * look for resource bundle resources |
| * |
| * @param locatorPathsSet set of locator paths to no longer check |
| */ |
| public void unregisterLocatorPaths(Set<LocatorPaths> locatorPathsSet) { |
| this.locatorPaths.removeAll(locatorPathsSet); |
| clearCache(); |
| } |
| |
| private ResourceResolver createResourceResolver() throws LoginException { |
| return resourceResolverFactory.getServiceResourceResolver(null); |
| } |
| |
| // ---------- ResourceBundleProvider --------------------------------------- |
| |
| /** |
| * Returns the configured default <code>Locale</code> which is used as a |
| * fallback for {@link #getResourceBundle(Locale)} and also as the basis for |
| * any messages requested from resource bundles. |
| */ |
| @Override |
| public Locale getDefaultLocale() { |
| return defaultLocale; |
| } |
| |
| /** |
| * Returns the <code>ResourceBundle</code> for the given |
| * <code>locale</code>. |
| * |
| * @param locale The <code>Locale</code> for which to return the resource |
| * bundle. If this is <code>null</code> the configured |
| * {@link #getDefaultLocale() default locale} is assumed. |
| * @return The <code>ResourceBundle</code> for the given locale. |
| * @throws MissingResourceException If the <code>ResourceResolver</code> |
| * is not available to access the resources. |
| */ |
| @Override |
| public ResourceBundle getResourceBundle(final Locale locale) { |
| return getResourceBundle(null, locale); |
| } |
| |
| @Override |
| public ResourceBundle getResourceBundle(final String baseName, Locale locale) { |
| return getResourceBundleInternal(null, baseName, locale); |
| } |
| |
| // ---------- ResourceChangeListener ------------------------------------------------ |
| |
| private static final class ChangeStatus { |
| public ResourceResolver resourceResolver; |
| public boolean reloadAll = false; |
| public final Set<JcrResourceBundle> reloadBundles = new HashSet<>(); |
| } |
| |
| @Override |
| public void onChange(final List<ResourceChange> changes) { |
| final ChangeStatus status = new ChangeStatus(); |
| try { |
| for (final ResourceChange change : changes) { |
| |
| if (!this.pathFilter.includePath(change.getPath())) { |
| continue; |
| } |
| this.onChange(status, change); |
| // if we need to reload all, we can skip all other events |
| if ( status.reloadAll ) { |
| break; |
| } |
| } |
| if ( status.reloadAll ) { |
| this.scheduleReloadBundles(true); |
| } else { |
| for(final JcrResourceBundle bundle : status.reloadBundles ) { |
| this.scheduleReloadBundle(bundle); |
| } |
| } |
| } catch ( final LoginException le) { |
| log.error("Unable to get service resource resolver.", le); |
| } finally { |
| if ( status.resourceResolver != null ) { |
| status.resourceResolver.close(); |
| } |
| } |
| } |
| |
| private void onChange(final ChangeStatus status, final ResourceChange change) |
| throws LoginException { |
| log.debug("onChange: Detecting change {} for path '{}'", change.getType(), change.getPath()); |
| |
| // if this change was on languageRootPath level this might change basename and locale as well, therefore |
| // invalidate everything |
| if (languageRootPaths.contains(change.getPath())) { |
| log.debug( |
| "onChange: Detected change of cached language root '{}', removing all cached ResourceBundles", |
| change.getPath()); |
| status.reloadAll = true; |
| } else { |
| for (final String root : languageRootPaths) { |
| if (change.getPath().startsWith(root)) { |
| // figure out which JcrResourceBundles from the cached ones is affected |
| for (JcrResourceBundle bundle : resourceBundleCache.values()) { |
| if (bundle.getLanguageRootPaths().contains(root)) { |
| // reload it |
| log.debug("onChange: Resource changes below '{}', reloading ResourceBundle '{}'", |
| root, bundle); |
| status.reloadBundles.add(bundle); |
| } |
| } |
| } |
| } |
| |
| // may be a completely new dictionary |
| if ( status.resourceResolver == null ) { |
| status.resourceResolver = createResourceResolver() ; |
| } |
| if (isDictionaryResource(status.resourceResolver, change)) { |
| status.reloadAll = true; |
| } |
| } |
| } |
| |
| |
| private boolean isDictionaryResource(final ResourceResolver resolver, final ResourceChange change) { |
| // language node changes happen quite frequently (https://issues.apache.org/jira/browse/SLING-2881) |
| // therefore only consider changes either for sling:MessageEntry's |
| // or for JSON dictionaries |
| // get valuemap |
| final Resource resource = resolver.getResource(change.getPath()); |
| if (resource == null) { |
| log.trace("Could not get resource for '{}' for event {}", change.getPath(), change.getType()); |
| return false; |
| } |
| if ( resource.getResourceType() == null ) { |
| return false; |
| } |
| if (resource.isResourceType(JcrResourceBundle.RT_MESSAGE_ENTRY)) { |
| log.debug("Found new dictionary entry: New {} resource in '{}' detected", JcrResourceBundle.RT_MESSAGE_ENTRY, change.getPath()); |
| return true; |
| } |
| final ValueMap valueMap = resource.getValueMap(); |
| // FIXME: derivatives from mix:Message are not detected |
| if (hasMixin(valueMap, JcrResourceBundle.MIXIN_MESSAGE)) { |
| log.debug("Found new dictionary entry: New {} resource in '{}' detected", JcrResourceBundle.MIXIN_MESSAGE, change.getPath()); |
| return true; |
| } |
| if (change.getPath().endsWith(".json")) { |
| // check for mixin |
| if (hasMixin(valueMap, JcrResourceBundle.MIXIN_LANGUAGE)) { |
| log.debug("Found new dictionary: New {} resource in '{}' detected", JcrResourceBundle.MIXIN_LANGUAGE, change.getPath()); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private boolean hasMixin(ValueMap valueMap, String mixin) { |
| final String[] mixins = valueMap.get(JcrResourceBundle.PROP_MIXINS, String[].class); |
| if ( mixins != null ) { |
| for(final String m : mixins) { |
| if (mixin.equals(m) ) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| private void scheduleReloadBundles(final boolean withDelay) { |
| // cancel all reload individual bundle jobs! |
| synchronized(scheduledJobNames) { |
| for (String scheduledJobName : scheduledJobNames) { |
| scheduler.unschedule(scheduledJobName); |
| } |
| } |
| scheduledJobNames.clear(); |
| // defer this job |
| final ScheduleOptions options; |
| if (withDelay) { |
| options = scheduler.AT(new Date(System.currentTimeMillis() + this.invalidationDelay)); |
| } else { |
| options = scheduler.NOW(); |
| } |
| options.name("ResourceBundleProvider: reload all resource bundles"); |
| scheduler.schedule(new Runnable() { |
| @Override |
| public void run() { |
| log.info("Reloading all resource bundles"); |
| clearCache(); |
| preloadBundles(); |
| } |
| }, options); |
| } |
| |
| private void scheduleReloadBundle(final JcrResourceBundle bundle) { |
| final Key key = new Key(bundle.getBaseName(), bundle.getLocale()); |
| |
| // defer this job |
| ScheduleOptions options = scheduler.AT(new Date(System.currentTimeMillis() + this.invalidationDelay)); |
| final String jobName = "ResourceBundleProvider: reload bundle with key " + key.toString(); |
| scheduledJobNames.add(jobName); |
| options.name(jobName); |
| scheduler.schedule(new Runnable() { |
| @Override |
| public void run() { |
| reloadBundle(key); |
| scheduledJobNames.remove(jobName); |
| } |
| }, options); |
| } |
| |
| void reloadBundle(final Key key) { |
| log.info("Reloading resource bundle for {}", key); |
| if (!this.preloadBundles) { |
| // remove bundle from cache |
| resourceBundleCache.remove(key); |
| // unregister bundle |
| unregisterResourceBundle(key); |
| } |
| |
| Collection<JcrResourceBundle> dependentBundles = new ArrayList<>(); |
| // this bundle might be a parent of a cached bundle -> invalidate those dependent bundles as well |
| for (JcrResourceBundle bundle : resourceBundleCache.values()) { |
| if (bundle.getParent() instanceof JcrResourceBundle) { |
| JcrResourceBundle parentBundle = (JcrResourceBundle) bundle.getParent(); |
| Key parentKey = new Key(parentBundle.getBaseName(), parentBundle.getLocale()); |
| if (parentKey.equals(key)) { |
| log.debug("Also invalidate dependent bundle {} which has bundle {} as parent", bundle, parentBundle); |
| dependentBundles.add(bundle); |
| } |
| } |
| } |
| for (JcrResourceBundle dependentBundle : dependentBundles) { |
| reloadBundle(new Key(dependentBundle.getBaseName(), dependentBundle.getLocale())); |
| } |
| |
| if (this.preloadBundles) { |
| // reload the bundle from the repository (will also fill cache and register as a service) |
| getResourceBundleInternal(null, key.baseName, key.locale, true); |
| } |
| } |
| |
| // ---------- SCR Integration ---------------------------------------------- |
| |
| /** |
| * Activates and configures this component with the repository access |
| * details and the default locale to use |
| * @throws LoginException |
| */ |
| @Activate |
| protected void activate(final BundleContext context, final Config config) throws LoginException { |
| this.defaultLocale = toLocale(config.locale_default()); |
| this.preloadBundles = config.preload_bundles(); |
| this.invalidationDelay = config.invalidation_delay(); |
| this.pathFilter = new PathFilter(config.included_paths(), config.excluded_paths()); |
| this.bundleContext = context; |
| |
| this.locatorPathsTracker = new BundleTracker<>(this.bundleContext, |
| Bundle.ACTIVE, new LocatorPathsTracker(this)); |
| this.locatorPathsTracker.open(); |
| |
| if (this.resourceResolverFactory != null) { // this is only null during test execution! |
| scheduleReloadBundles(false); |
| } |
| } |
| |
| @Deactivate |
| protected void deactivate() { |
| if (this.locatorPathsTracker != null) { |
| this.locatorPathsTracker.close(); |
| this.locatorPathsTracker = null; |
| } |
| |
| clearCache(); |
| this.bundleContext = null; |
| } |
| |
| // ---------- internal ----------------------------------------------------- |
| |
| /** |
| * Internal implementation of the {@link #getResourceBundle(Locale)} method |
| * employing the cache of resource bundles. Creates the bundle if not |
| * already cached. |
| * |
| * @throws MissingResourceException If the resource bundles needs to be |
| * created and the <code>ResourceResolver</code> is not |
| * available to access the resources. |
| */ |
| private ResourceBundle getResourceBundleInternal(ResourceResolver optionalResolver, String baseName, Locale locale) { |
| return getResourceBundleInternal(optionalResolver, baseName, locale, false); |
| } |
| |
| private ResourceBundle getResourceBundleInternal(ResourceResolver optionalResolver, final String baseName, Locale locale, final boolean overwriteCache) { |
| if (locale == null) { |
| locale = defaultLocale; |
| } |
| |
| final Key key = new Key(baseName, locale); |
| JcrResourceBundle resourceBundle = !overwriteCache ? resourceBundleCache.get(key) : null; |
| if (resourceBundle != null) { |
| log.debug("getResourceBundleInternal({}): got cache hit on first try", key); |
| } else { |
| if (loadingGuards.get(key) == null) { |
| loadingGuards.putIfAbsent(key, new Semaphore(1)); |
| } |
| final Semaphore loadingGuard = loadingGuards.get(key); |
| try { |
| loadingGuard.acquire(); |
| resourceBundle = !overwriteCache ? resourceBundleCache.get(key) : null; |
| if (resourceBundle != null) { |
| log.debug("getResourceBundleInternal({}): got cache hit on second try", key); |
| } else { |
| log.debug("getResourceBundleInternal({}): reading from Repository", key); |
| ResourceResolver localResolver = null; |
| try { |
| if ( optionalResolver == null ) { |
| localResolver = createResourceResolver(); |
| optionalResolver = localResolver; |
| } |
| |
| resourceBundle = createResourceBundle(optionalResolver, key.baseName, key.locale); |
| // put the newly created ResourceBundle to the cache. If it replaces an existing entry unregister the existing |
| // service registration first before re-registering the new ResourceBundle. |
| if (resourceBundleCache.put(key, resourceBundle) != null) { |
| unregisterResourceBundle(key); |
| } |
| registerResourceBundle(key, resourceBundle); |
| |
| } catch ( final LoginException le) { |
| throw (MissingResourceException)new MissingResourceException("Unable to create service resource resolver", |
| baseName, |
| locale.toString()).initCause(le); |
| } finally { |
| if ( localResolver != null ) { |
| localResolver.close(); |
| } |
| } |
| } |
| } catch (InterruptedException e) { |
| Thread.interrupted(); |
| } finally { |
| loadingGuard.release(); |
| } |
| } |
| log.trace("getResourceBundleInternal({}) ==> {}", key, resourceBundle); |
| return resourceBundle; |
| } |
| |
| private void unregisterResourceBundle(Key key) { |
| ServiceRegistration<ResourceBundle> serviceRegistration = null; |
| synchronized (this) { |
| serviceRegistration = bundleServiceRegistrations.remove(key); |
| } |
| if (serviceRegistration != null) { |
| serviceRegistration.unregister(); |
| } else { |
| log.warn("Could not find resource bundle service for {}", key); |
| } |
| } |
| |
| private void registerResourceBundle(Key key, JcrResourceBundle resourceBundle) { |
| Dictionary<String, Object> serviceProps = new Hashtable<>(); |
| if (key.baseName != null) { |
| serviceProps.put("baseName", key.baseName); |
| } |
| serviceProps.put("locale", key.locale.toString()); |
| ServiceRegistration<ResourceBundle> serviceReg = bundleContext.registerService(ResourceBundle.class, |
| resourceBundle, serviceProps); |
| synchronized (this) { |
| bundleServiceRegistrations.put(key, serviceReg); |
| } |
| |
| // register language root paths |
| final Set<String> languageRoots = resourceBundle.getLanguageRootPaths(); |
| this.languageRootPaths.addAll(languageRoots); |
| |
| log.debug("registerResourceBundle({}, ...): added service registration and language roots {}", key, languageRoots); |
| log.info("Currently loaded dictionaries across all locales: {}", languageRootPaths); |
| } |
| |
| /** |
| * Creates the resource bundle for the give locale. |
| * |
| * @throws MissingResourceException If the <code>ResourceResolver</code> |
| * is not available to access the resources. |
| */ |
| private JcrResourceBundle createResourceBundle(final ResourceResolver resolver, final String baseName, final Locale locale) { |
| final JcrResourceBundle bundle = new JcrResourceBundle(locale, baseName, resolver, locatorPaths, this.pathFilter); |
| |
| // set parent resource bundle |
| Locale parentLocale = getParentLocale(locale); |
| if (parentLocale != null) { |
| bundle.setParent(getResourceBundleInternal(resolver, baseName, parentLocale)); |
| } else { |
| bundle.setParent(getRootResourceBundle()); |
| } |
| |
| return bundle; |
| } |
| |
| /** |
| * Returns the parent locale of the given locale. The parent locale is the |
| * locale of a locale is defined as follows: |
| * <ol> |
| * <li>If the locale has an variant, the parent locale is the locale with |
| * the same language and country without the variant.</li> |
| * <li>If the locale has no variant but a country, the parent locale is the |
| * locale with the same language but neither country nor variant.</li> |
| * <li>If the locale has no country and not variant and whose language is |
| * different from the language of the the configured default locale, the |
| * parent locale is the configured default locale.</li> |
| * <li>Otherwise there is no parent locale and <code>null</code> is |
| * returned.</li> |
| * </ol> |
| */ |
| private Locale getParentLocale(Locale locale) { |
| if (locale.getVariant().length() != 0) { |
| return new Locale(locale.getLanguage(), locale.getCountry()); |
| } else if (locale.getCountry().length() != 0) { |
| return new Locale(locale.getLanguage()); |
| } else if (!locale.getLanguage().equals(defaultLocale.getLanguage())) { |
| return defaultLocale; |
| } |
| |
| // no more parents |
| return null; |
| } |
| |
| /** |
| * Returns a ResourceBundle which is used as the root resource bundle, that |
| * is the ultimate parent: |
| * <ul> |
| * <li><code>getLocale()</code> returns Locale("", "", "")</li> |
| * <li><code>handleGetObject(String key)</code> returns the <code>key</code></li> |
| * <li><code>getKeys()</code> returns an empty enumeration. |
| * </ul> |
| * |
| * @return The root resource bundle |
| */ |
| private ResourceBundle getRootResourceBundle() { |
| if (rootResourceBundle == null) { |
| rootResourceBundle = new RootResourceBundle(); |
| } |
| return rootResourceBundle; |
| } |
| |
| private void clearCache() { |
| resourceBundleCache.clear(); |
| languageRootPaths.clear(); |
| |
| final List<ServiceRegistration<ResourceBundle>> regs; |
| synchronized (this) { |
| regs = new ArrayList<>(bundleServiceRegistrations.values()); |
| bundleServiceRegistrations.clear(); |
| } |
| for (final ServiceRegistration<ResourceBundle> serviceReg : regs) { |
| serviceReg.unregister(); |
| } |
| } |
| |
| private void preloadBundles() { |
| if (this.preloadBundles) { |
| try ( final ResourceResolver resolver = createResourceResolver() ) { |
| final Iterator<Map<String, Object>> bundles = resolver.queryResources( |
| JcrResourceBundle.QUERY_LANGUAGE_ROOTS, "xpath"); |
| final Set<Key> usedKeys = new HashSet<>(); |
| while (bundles.hasNext()) { |
| final Map<String,Object> bundle = bundles.next(); |
| if (bundle.containsKey(PROP_LANGUAGE) && bundle.containsKey(PROP_PATH)) { |
| final String path = bundle.get(PROP_PATH).toString(); |
| final String language = bundle.get(PROP_LANGUAGE).toString(); |
| if (this.pathFilter.includePath(path)) { |
| final Locale locale = toLocale(language); |
| final String baseName = bundle.containsKey(PROP_BASENAME) ? bundle.get(PROP_BASENAME).toString() : null; |
| final Key key = new Key(baseName, locale); |
| if (usedKeys.add(key)) { |
| getResourceBundleInternal(resolver, baseName, locale); |
| } |
| } else { |
| log.warn("Ignoring i18n bundle for language {} at {} because it is not included by the path filter", language, path); |
| } |
| } |
| } |
| } catch ( final LoginException le) { |
| log.error("Unable to create service user resource resolver.", le); |
| } |
| } |
| } |
| |
| /** |
| * Converts the given <code>localeString</code> to a valid |
| * <code>java.util.Locale</code>. It must either be in the format specified by |
| * {@link Locale#toString()} or in <a href="https://tools.ietf.org/html/bcp47">BCP 47 format</a> |
| * If the locale string is <code>null</code> or empty, the platform default locale is assumed. If |
| * the localeString matches any locale available per default on the |
| * platform, that platform locale is returned. Otherwise the localeString is |
| * parsed and the language and country parts are compared against the |
| * languages and countries provided by the platform. Any unsupported |
| * language or country is replaced by the platform default language and |
| * country. |
| * @param localeString the locale as string |
| * @return the {@link Locale} being generated from the {@code localeString} |
| */ |
| static Locale toLocale(String localeString) { |
| if (localeString == null || localeString.length() == 0) { |
| return Locale.getDefault(); |
| } |
| // support BCP 47 compliant strings as well (using a different separator "-" instead of "_") |
| localeString = localeString.replaceAll("-", "_"); |
| |
| // check language and country |
| final String[] parts = localeString.split("_"); |
| if (parts.length == 0) { |
| return Locale.getDefault(); |
| } |
| |
| // at least language is available |
| String lang = parts[0]; |
| boolean isValidLanguageCode = false; |
| String[] langs = Locale.getISOLanguages(); |
| for (int i = 0; i < langs.length; i++) { |
| if (langs[i].equalsIgnoreCase(lang)) { |
| isValidLanguageCode = true; |
| break; |
| } |
| } |
| if (!isValidLanguageCode) { |
| lang = Locale.getDefault().getLanguage(); |
| } |
| |
| // only language |
| if (parts.length == 1) { |
| return new Locale(lang); |
| } |
| |
| // country is also available |
| String country = parts[1]; |
| boolean isValidCountryCode = false; |
| // allow user-assigned codes (https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#User-assigned_code_elements) |
| if (USER_ASSIGNED_COUNTRY_CODES_PATTERN.matcher(country.toLowerCase()).matches()) { |
| isValidCountryCode = true; |
| } else { |
| String[] countries = Locale.getISOCountries(); |
| for (int i = 0; i < countries.length; i++) { |
| if (countries[i].equalsIgnoreCase(country)) { |
| isValidCountryCode = true; // signal ok |
| break; |
| } |
| } |
| } |
| if (!isValidCountryCode) { |
| country = Locale.getDefault().getCountry(); |
| } |
| |
| // language and country |
| if (parts.length == 2) { |
| return new Locale(lang, country); |
| } |
| |
| // language, country and variant |
| return new Locale(lang, country, parts[2]); |
| } |
| |
| //---------- internal class |
| |
| /** |
| * The <code>Key</code> class encapsulates the base name and Locale in a |
| * single object that can be used as the key in a <code>HashMap</code>. |
| */ |
| protected static final class Key { |
| |
| final String baseName; |
| |
| final Locale locale; |
| |
| // precomputed hash code, because this will always be used due to |
| // this instance being used as a key in a HashMap. |
| private final int hashCode; |
| |
| Key(final String baseName, final Locale locale) { |
| |
| int hc = 0; |
| if (baseName != null) { |
| hc += 17 * baseName.hashCode(); |
| } |
| if (locale != null) { |
| hc += 13 * locale.hashCode(); |
| } |
| |
| this.baseName = baseName; |
| this.locale = locale; |
| this.hashCode = hc; |
| } |
| |
| @Override |
| public int hashCode() { |
| return hashCode; |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (obj == this) { |
| return true; |
| } else if (obj instanceof Key) { |
| Key other = (Key) obj; |
| return equals(this.baseName, other.baseName) |
| && equals(this.locale, other.locale); |
| } |
| |
| return false; |
| } |
| |
| private static boolean equals(Object o1, Object o2) { |
| if (o1 == null) { |
| if (o2 != null) { |
| return false; |
| } |
| } else if (!o1.equals(o2)) { |
| return false; |
| } |
| return true; |
| } |
| |
| @Override |
| public String toString() { |
| return "Key(" + baseName + ", " + locale + ")"; |
| } |
| } |
| } |