1 /*
2  * Hibernate Validator, declare and validate application constraints
3  *
4  * License: Apache License, Version 2.0
5  * See the license.txt file in the root directory or <http://www.apache.org/licenses/LICENSE-2.0>.
6  */

7 package org.hibernate.validator.resourceloading;
8
9 import static org.hibernate.validator.internal.util.CollectionHelper.newHashSet;
10 import static org.hibernate.validator.internal.util.logging.Messages.MESSAGES;
11
12 import java.io.IOException;
13 import java.lang.invoke.MethodHandles;
14 import java.lang.reflect.Method;
15 import java.net.URL;
16 import java.security.AccessController;
17 import java.security.PrivilegedAction;
18 import java.util.Collections;
19 import java.util.Enumeration;
20 import java.util.Locale;
21 import java.util.Map;
22 import java.util.MissingResourceException;
23 import java.util.Properties;
24 import java.util.ResourceBundle;
25 import java.util.Set;
26
27 import org.hibernate.validator.Incubating;
28 import org.hibernate.validator.internal.util.CollectionHelper;
29 import org.hibernate.validator.internal.util.Contracts;
30 import org.hibernate.validator.internal.util.logging.Log;
31 import org.hibernate.validator.internal.util.logging.LoggerFactory;
32 import org.hibernate.validator.internal.util.privilegedactions.GetClassLoader;
33 import org.hibernate.validator.internal.util.privilegedactions.GetMethod;
34 import org.hibernate.validator.internal.util.privilegedactions.GetResources;
35 import org.hibernate.validator.internal.util.stereotypes.Immutable;
36 import org.hibernate.validator.spi.resourceloading.ResourceBundleLocator;
37
38 /**
39  * A resource bundle locator, that loads resource bundles by invoking {@code ResourceBundle.loadBundle(String, Local, ClassLoader)}.
40  * <p>
41  * This locator is also able to load all property files of a given name (in case there are multiple with the same
42  * name on the classpath) and aggregates them into a {@code ResourceBundle}.
43  *
44  * @author Hardy Ferentschik
45  * @author Gunnar Morling
46  * @author Guillaume Smet
47  */

48 public class PlatformResourceBundleLocator implements ResourceBundleLocator {
49
50     private static final Log LOG = LoggerFactory.make( MethodHandles.lookup() );
51     private static final boolean RESOURCE_BUNDLE_CONTROL_INSTANTIABLE = determineAvailabilityOfResourceBundleControl();
52
53     private final String bundleName;
54     private final ClassLoader classLoader;
55     private final boolean aggregate;
56
57     @Immutable
58     private final Map<Locale, ResourceBundle> preloadedResourceBundles;
59
60     /**
61      * Creates a new {@link PlatformResourceBundleLocator}.
62      *
63      * @param bundleName the name of the bundle to load
64      */

65     public PlatformResourceBundleLocator(String bundleName) {
66         this( bundleName, Collections.emptySet() );
67     }
68
69     /**
70      * Creates a new {@link PlatformResourceBundleLocator}.
71      *
72      * @param bundleName the name of the bundle to load
73      * @param classLoader the classloader to be used for loading the bundle. If {@code null}, the current thread context
74      * classloader and finally Hibernate Validator's own classloader will be used for loading the specified
75      * bundle.
76      *
77      * @since 5.2
78      */

79     public PlatformResourceBundleLocator(String bundleName, ClassLoader classLoader) {
80         this( bundleName, Collections.emptySet(), classLoader );
81     }
82
83     /**
84      * Creates a new {@link PlatformResourceBundleLocator}.
85      *
86      * @param bundleName the name of the bundle to load
87      * @param classLoader the classloader to be used for loading the bundle. If {@code null}, the current thread context
88      * classloader and finally Hibernate Validator's own classloader will be used for loading the specified
89      * bundle.
90      * @param aggregate Whether or not all resource bundles of a given name should be loaded and potentially merged.
91      *
92      * @since 5.2
93      */

94     public PlatformResourceBundleLocator(String bundleName, ClassLoader classLoader, boolean aggregate) {
95         this( bundleName, Collections.emptySet(), classLoader, aggregate );
96     }
97
98     /**
99      * Creates a new {@link PlatformResourceBundleLocator}.
100      *
101      * @param bundleName the name of the bundle to load
102      * @param localesToInitialize the set of locales to initialize at bootstrap
103      *
104      * @since 6.1.1
105      */

106     @Incubating
107     public PlatformResourceBundleLocator(String bundleName, Set<Locale> localesToInitialize) {
108         this( bundleName, localesToInitialize, null );
109     }
110
111     /**
112      * Creates a new {@link PlatformResourceBundleLocator}.
113      *
114      * @param bundleName the name of the bundle to load
115      * @param localesToInitialize the set of locales to initialize at bootstrap
116      * @param classLoader the classloader to be used for loading the bundle. If {@code null}, the current thread context
117      * classloader and finally Hibernate Validator's own classloader will be used for loading the specified
118      * bundle.
119      *
120      * @since 6.1.1
121      */

122     @Incubating
123     public PlatformResourceBundleLocator(String bundleName, Set<Locale> localesToInitialize, ClassLoader classLoader) {
124         this( bundleName, localesToInitialize, classLoader, false );
125     }
126
127     /**
128      * Creates a new {@link PlatformResourceBundleLocator}.
129      *
130      * @param bundleName the name of the bundle to load
131      * @param localesToInitialize the set of locales to initialize at bootstrap
132      * @param classLoader the classloader to be used for loading the bundle. If {@code null}, the current thread context
133      * classloader and finally Hibernate Validator's own classloader will be used for loading the specified
134      * bundle.
135      * @param aggregate Whether or not all resource bundles of a given name should be loaded and potentially merged.
136      *
137      * @since 6.1
138      */

139     @Incubating
140     public PlatformResourceBundleLocator(String bundleName,
141             Set<Locale> localesToInitialize,
142             ClassLoader classLoader,
143             boolean aggregate) {
144         Contracts.assertNotNull( bundleName, "bundleName" );
145
146         this.bundleName = bundleName;
147         this.classLoader = classLoader;
148
149         this.aggregate = aggregate && RESOURCE_BUNDLE_CONTROL_INSTANTIABLE;
150
151         if ( !localesToInitialize.isEmpty() ) {
152             Map<Locale, ResourceBundle> tmpPreloadedResourceBundles = CollectionHelper.newHashMap( localesToInitialize.size() );
153             for ( Locale localeToPreload : localesToInitialize ) {
154                 tmpPreloadedResourceBundles.put( localeToPreload, doGetResourceBundle( localeToPreload ) );
155             }
156             this.preloadedResourceBundles = CollectionHelper.toImmutableMap( tmpPreloadedResourceBundles );
157         }
158         else {
159             this.preloadedResourceBundles = Collections.emptyMap();
160         }
161     }
162
163     /**
164      * Search current thread classloader for the resource bundle. If not found,
165      * search validator (this) classloader.
166      *
167      * @param locale The locale of the bundle to load.
168      *
169      * @return the resource bundle or {@code nullif none is found.
170      */

171     @Override
172     public ResourceBundle getResourceBundle(Locale locale) {
173         if ( !preloadedResourceBundles.isEmpty() ) {
174             // we need to use containsKey() as the cached resource bundle can be null
175             if ( preloadedResourceBundles.containsKey( locale ) ) {
176                 return preloadedResourceBundles.get( locale );
177             }
178             else {
179                 throw LOG.uninitializedLocale( locale );
180             }
181         }
182
183         return doGetResourceBundle( locale );
184     }
185
186     private ResourceBundle doGetResourceBundle(Locale locale) {
187         ResourceBundle rb = null;
188
189         if ( classLoader != null ) {
190             rb = loadBundle(
191                     classLoader, locale, bundleName
192                             + " not found by user-provided classloader"
193             );
194         }
195
196         if ( rb == null ) {
197             ClassLoader classLoader = run( GetClassLoader.fromContext() );
198             if ( classLoader != null ) {
199                 rb = loadBundle(
200                         classLoader, locale, bundleName
201                                 + " not found by thread context classloader"
202                 );
203             }
204         }
205
206         if ( rb == null ) {
207             ClassLoader classLoader = run( GetClassLoader.fromClass( PlatformResourceBundleLocator.class ) );
208             rb = loadBundle(
209                     classLoader, locale, bundleName
210                             + " not found by validator classloader"
211             );
212         }
213         if ( rb != null ) {
214             LOG.debugf( "%s found.", bundleName );
215         }
216         else {
217             LOG.debugf( "%s not found.", bundleName );
218         }
219         return rb;
220     }
221
222     private ResourceBundle loadBundle(ClassLoader classLoader, Locale locale, String message) {
223         ResourceBundle rb = null;
224         try {
225             if ( aggregate ) {
226                 rb = ResourceBundle.getBundle(
227                         bundleName,
228                         locale,
229                         classLoader,
230                         AggregateResourceBundle.CONTROL
231                 );
232             }
233             else {
234                 rb = ResourceBundle.getBundle(
235                         bundleName,
236                         locale,
237                         classLoader
238                 );
239             }
240         }
241         catch (MissingResourceException e) {
242             LOG.trace( message );
243         }
244         return rb;
245     }
246
247     /**
248      * Runs the given privileged action, using a privileged block if required.
249      * <p>
250      * <b>NOTE:</b> This must never be changed into a publicly available method to avoid execution of arbitrary
251      * privileged actions within HV's protection domain.
252      */

253     private static <T> T run(PrivilegedAction<T> action) {
254         return System.getSecurityManager() != null ? AccessController.doPrivileged( action ) : action.run();
255     }
256
257     /**
258      * Check whether ResourceBundle.Control is available, which is needed for bundle aggregation. If not, we'll skip
259      * resource aggregation.
260      * <p>
261      * It is *not* available
262      * <ul>
263      * <li>in the Google App Engine environment</li>
264      * <li>when running HV as Java 9 named module (which would be the case when adding a module-info descriptor to the
265      * HV JAR)</li>
266      * </ul>
267      *
268      * @see <a href="http://code.google.com/appengine/docs/java/jrewhitelist.html">GAE JRE whitelist</a>
269      * @see <a href="https://hibernate.atlassian.net/browse/HV-1023">HV-1023</a>
270      * @see <a href="http://download.java.net/java/jdk9/docs/api/java/util/ResourceBundle.Control.html">ResourceBundle.Control</a>
271      */

272     private static boolean determineAvailabilityOfResourceBundleControl() {
273         try {
274             ResourceBundle.Control dummyControl = AggregateResourceBundle.CONTROL;
275
276             if ( dummyControl == null ) {
277                 return false;
278             }
279
280             Method getModule = run( GetMethod.action( Class.class"getModule" ) );
281             // not on Java 9
282             if ( getModule == null ) {
283                 return true;
284             }
285
286             // on Java 9, check whether HV is a named module
287             Object module = getModule.invoke( PlatformResourceBundleLocator.class );
288             Method isNamedMethod = run( GetMethod.action( module.getClass(), "isNamed" ) );
289             boolean isNamed = (Boolean) isNamedMethod.invoke( module );
290
291             return !isNamed;
292         }
293         catch (Throwable e) {
294             LOG.info( MESSAGES.unableToUseResourceBundleAggregation() );
295             return false;
296         }
297     }
298
299     /**
300      * Inspired by <a href="http://stackoverflow.com/questions/4614465/is-it-possible-to-include-resource-bundle-files-within-a-resource-bundle">this</a>
301      * Stack Overflow question.
302      */

303     private static class AggregateResourceBundle extends ResourceBundle {
304
305         protected static final Control CONTROL = new AggregateResourceBundleControl();
306         private final Properties properties;
307
308         protected AggregateResourceBundle(Properties properties) {
309             this.properties = properties;
310         }
311
312         @Override
313         protected Object handleGetObject(String key) {
314             return properties.get( key );
315         }
316
317         @Override
318         public Enumeration<String> getKeys() {
319             Set<String> keySet = newHashSet();
320             keySet.addAll( properties.stringPropertyNames() );
321             if ( parent != null ) {
322                 keySet.addAll( Collections.list( parent.getKeys() ) );
323             }
324             return Collections.enumeration( keySet );
325         }
326     }
327
328     private static class AggregateResourceBundleControl extends ResourceBundle.Control {
329         @Override
330         public ResourceBundle newBundle(
331                 String baseName,
332                 Locale locale,
333                 String format,
334                 ClassLoader loader,
335                 boolean reload)
336                 throws IllegalAccessException, InstantiationException, IOException {
337             // only *.properties files can be aggregated. Other formats are delegated to the default implementation
338             if ( !"java.properties".equals( format ) ) {
339                 return super.newBundle( baseName, locale, format, loader, reload );
340             }
341
342             String resourceName = toBundleName( baseName, locale ) + ".properties";
343             Properties properties = load( resourceName, loader );
344             return properties.size() == 0 ? null : new AggregateResourceBundle( properties );
345         }
346
347         private Properties load(String resourceName, ClassLoader loader) throws IOException {
348             Properties aggregatedProperties = new Properties();
349             Enumeration<URL> urls = run( GetResources.action( loader, resourceName ) );
350             while ( urls.hasMoreElements() ) {
351                 URL url = urls.nextElement();
352                 Properties properties = new Properties();
353                 properties.load( url.openStream() );
354                 aggregatedProperties.putAll( properties );
355             }
356             return aggregatedProperties;
357         }
358     }
359 }
360