/* * 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.util.Collections; import java.util.Enumeration; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.ResourceBundle; import java.util.TreeMap; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.wrappers.SlingHttpServletRequestWrapper; import org.apache.sling.commons.osgi.Order; import org.apache.sling.commons.osgi.ServiceUtil; import org.apache.sling.i18n.DefaultLocaleResolver; import org.apache.sling.i18n.LocaleResolver; import org.apache.sling.i18n.RequestLocaleResolver; import org.apache.sling.i18n.ResourceBundleProvider; import org.osgi.framework.Constants; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.ReferenceCardinality; import org.osgi.service.component.annotations.ReferencePolicy; import org.osgi.service.component.annotations.ReferencePolicyOption; import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The <code>I18NFilter</code> class is a request level filter, which provides * the resource bundle for the current request. */ @Component(service = Filter.class, property = { Constants.SERVICE_DESCRIPTION + "=Internationalization Support Filter", Constants.SERVICE_VENDOR + "=The Apache Software Foundation", Constants.SERVICE_RANKING + ":Integer=700", "sling.filter.scope=REQUEST", "sling.filter.scope=ERROR", HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_PATTERN + "=/", HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT + "=(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=*)" }) public class I18NFilter implements Filter { /** * The default server default locale if not configured <code>Locale.ENGLISH</code>. */ private static final Locale DEFAULT_LOCALE = Locale.ENGLISH; /** Logger */ private final static Logger LOG = LoggerFactory.getLogger(I18NFilter.class.getName()); private final DefaultLocaleResolver DEFAULT_LOCALE_RESOLVER = new DefaultLocaleResolver(); private volatile LocaleResolver localeResolver = DEFAULT_LOCALE_RESOLVER; private volatile RequestLocaleResolver requestLocaleResolver = DEFAULT_LOCALE_RESOLVER; private final Map<Object, ResourceBundleProvider> providers = new TreeMap<>(); private volatile ResourceBundleProvider[] sortedProviders = new ResourceBundleProvider[0]; private final ResourceBundleProvider combinedProvider = new CombinedBundleProvider(); /** Count the number init() has been called. */ private volatile int initCount; /** * @see javax.servlet.Filter#init(javax.servlet.FilterConfig) */ @Override public void init(FilterConfig filterConfig) { synchronized(this) { initCount++; } } /** * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) */ @Override public void doFilter(ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException { final boolean runGlobal = this.initCount == 2; if ( request instanceof SlingHttpServletRequest ) { // check if we can use the simple version to wrap if ( !runGlobal || this.requestLocaleResolver == DEFAULT_LOCALE_RESOLVER ) { // wrap with our ResourceBundle provisioning request = new I18NSlingHttpServletRequest(request, combinedProvider, localeResolver); } else { request = new BaseI18NSlingHttpServletRequest(request, combinedProvider); } } else { request = new I18NHttpServletRequest(request, combinedProvider, requestLocaleResolver); } // and forward the request chain.doFilter(request, response); } /** * @see javax.servlet.Filter#destroy() */ @Override public void destroy() { synchronized(this) { initCount--; } } // ---------- SCR Integration ---------------------------------------------- @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, policyOption=ReferencePolicyOption.GREEDY) protected void bindLocaleResolver(final LocaleResolver resolver) { this.localeResolver = resolver; } protected void unbindLocaleResolver(final LocaleResolver resolver) { if (this.localeResolver == resolver) { this.localeResolver = DEFAULT_LOCALE_RESOLVER; } } @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, policyOption=ReferencePolicyOption.GREEDY) protected void bindRequestLocaleResolver(final RequestLocaleResolver resolver) { this.requestLocaleResolver = resolver; } protected void unbindRequestLocaleResolver(final RequestLocaleResolver resolver) { if (this.requestLocaleResolver == resolver) { this.requestLocaleResolver = DEFAULT_LOCALE_RESOLVER; } } @Reference(service = ResourceBundleProvider.class, cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) protected void bindResourceBundleProvider(final ResourceBundleProvider provider, final Map<String, Object> props) { synchronized ( this.providers ) { this.providers.put(ServiceUtil.getComparableForServiceRanking(props, Order.ASCENDING), provider); this.sortedProviders = this.providers.values().toArray(new ResourceBundleProvider[this.providers.size()]); } } protected void unbindResourceBundleProvider(final ResourceBundleProvider provider, final Map<String, Object> props) { synchronized ( this.providers ) { this.providers.remove(ServiceUtil.getComparableForServiceRanking(props, Order.ASCENDING)); this.sortedProviders = this.providers.values().toArray(new ResourceBundleProvider[this.providers.size()]); } } // ---------- internal ----------------------------------------------------- /** Provider that goes through a list of registered providers and takes the first non-null responses */ private class CombinedBundleProvider implements ResourceBundleProvider { @Override public Locale getDefaultLocale() { // ask all registered providers, use the first one that returns final ResourceBundleProvider[] providers = sortedProviders; for(int i=providers.length-1; i >= 0; i--) { final ResourceBundleProvider provider = providers[i]; final Locale locale = provider.getDefaultLocale(); if (locale != null) { return locale; } } return null; } @Override public ResourceBundle getResourceBundle(final Locale locale) { // ask all registered providers, use the first one that returns final ResourceBundleProvider[] providers = sortedProviders; for(int i=providers.length-1; i >= 0; i--) { final ResourceBundleProvider provider = providers[i]; final ResourceBundle bundle = provider.getResourceBundle(locale); if (bundle != null) { return bundle; } } return null; } @Override public ResourceBundle getResourceBundle(final String baseName, final Locale locale) { // ask all registered providers, use the first one that returns final ResourceBundleProvider[] providers = sortedProviders; for(int i=providers.length-1; i >= 0; i--) { final ResourceBundleProvider provider = providers[i]; final ResourceBundle bundle = provider.getResourceBundle(baseName, locale); if (bundle != null) { return bundle; } } return null; } } private static Locale defaultLocale(ResourceBundleProvider bundleProvider) { Locale defaultLocale = bundleProvider.getDefaultLocale(); return (defaultLocale != null) ? defaultLocale : DEFAULT_LOCALE; } // ---------- internal class ----------------------------------------------- private static class I18NHttpServletRequest extends HttpServletRequestWrapper { private final ResourceBundleProvider bundleProvider; private final RequestLocaleResolver localeResolver; private Locale locale; private List<Locale> localeList; private ResourceBundle resourceBundle; I18NHttpServletRequest(final ServletRequest delegatee, final ResourceBundleProvider bundleProvider, final RequestLocaleResolver localeResolver) { super((HttpServletRequest)delegatee); this.bundleProvider = bundleProvider; this.localeResolver = localeResolver; } @Override public Locale getLocale() { if (locale == null) { locale = this.getLocaleList().get(0); } return locale; } @Override public Enumeration<Locale> getLocales() { return Collections.enumeration(getLocaleList()); } @Override public Object getAttribute(final String name) { if ( ResourceBundleProvider.BUNDLE_REQ_ATTR.equals(name) ) { if ( this.resourceBundle == null && this.bundleProvider != null) { this.resourceBundle = this.bundleProvider.getResourceBundle(this.getLocale()); } return this.resourceBundle; } return super.getAttribute(name); } private List<Locale> getLocaleList() { if (localeList == null) { List<Locale> resolved = localeResolver.resolveLocale((HttpServletRequest)this.getRequest()); this.localeList = (resolved != null && !resolved.isEmpty()) ? resolved : Collections.singletonList(defaultLocale(this.bundleProvider)); } return localeList; } } private static class BaseI18NSlingHttpServletRequest extends SlingHttpServletRequestWrapper { protected final ResourceBundleProvider bundleProvider; BaseI18NSlingHttpServletRequest(final ServletRequest delegatee, final ResourceBundleProvider bundleProvider) { super((SlingHttpServletRequest) delegatee); this.bundleProvider = bundleProvider; } @Override public ResourceBundle getResourceBundle(Locale locale) { return getResourceBundle(null, locale); } @Override public ResourceBundle getResourceBundle(String baseName, Locale locale) { if (bundleProvider != null) { if (locale == null) { locale = getLocale(); } try { return bundleProvider.getResourceBundle(baseName, locale); } catch (MissingResourceException mre) { LOG.warn( "getResourceBundle: Cannot get ResourceBundle from provider", mre); } } else { LOG.info("getResourceBundle: ResourceBundleProvider not available, calling default implementation"); } return super.getResourceBundle(baseName, locale); } } private static class I18NSlingHttpServletRequest extends BaseI18NSlingHttpServletRequest { private final LocaleResolver localeResolver; private Locale locale; private List<Locale> localeList; I18NSlingHttpServletRequest(final ServletRequest delegatee, final ResourceBundleProvider bundleProvider, final LocaleResolver localeResolver) { super(delegatee, bundleProvider); this.localeResolver = localeResolver; } @Override public Object getAttribute(final String name) { if ( ResourceBundleProvider.BUNDLE_REQ_ATTR.equals(name) ) { final Object superValue = super.getAttribute(name); return (superValue != null ? superValue : this.getResourceBundle(null)); } return super.getAttribute(name); } @Override public Locale getLocale() { if (locale == null) { locale = this.getLocaleList().get(0); } return locale; } @Override public Enumeration<Locale> getLocales() { return Collections.enumeration(getLocaleList()); } private List<Locale> getLocaleList() { if (localeList == null) { List<Locale> resolved = localeResolver.resolveLocale(this.getSlingRequest()); this.localeList = (resolved != null && !resolved.isEmpty()) ? resolved : Collections.singletonList(defaultLocale(this.bundleProvider)); } return localeList; } } }