This is an automated email from the ASF dual-hosted git repository. vladimirsitnikov pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/calcite.git
commit 94e6272d57478a5913cb51e7df5c25e67511229a Author: Vladimir Sitnikov <[email protected]> AuthorDate: Mon Nov 11 11:27:14 2019 +0300 [CALCITE-2905] Add hydromatic-resource as plain source file See https://issues.apache.org/jira/browse/CALCITE-2905?focusedCommentId=16851698&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-16851698 --- .../java/org/apache/calcite/runtime/Resources.java | 1124 ++++++++++++++++++++ 1 file changed, 1124 insertions(+) diff --git a/core/src/main/java/org/apache/calcite/runtime/Resources.java b/core/src/main/java/org/apache/calcite/runtime/Resources.java new file mode 100644 index 0000000..a167500 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/runtime/Resources.java @@ -0,0 +1,1124 @@ +/* + * Licensed to Julian Hyde under one or more contributor license + * agreements. See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. Julian Hyde + * 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.calcite.runtime; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.*; +import java.text.*; +import java.util.*; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Defining wrapper classes around resources that allow the compiler to check + * whether the resources exist and have the argument types that your code + * expects. + * + * <p>If this class belongs to a package other than + * {@code net.hydromatic.resource}, it was probably generated by the Maven + * plugin (groupId: "net.hydromatic", artifactId: + * "hydromatic-resource-maven-plugin"). Code generation allows projects to use + * this resource library without adding a runtime dependency on another JAR. + */ +public class Resources { + private static final ThreadLocal<Locale> MAP_THREAD_TO_LOCALE = + new ThreadLocal<Locale>(); + + private Resources() {} + + /** Returns the preferred locale of the current thread, or + * the default locale if the current thread has not called + * {@link #setThreadLocale}. + * + * @return Locale */ + protected static Locale getThreadOrDefaultLocale() { + Locale locale = getThreadLocale(); + if (locale == null) { + return Locale.getDefault(); + } else { + return locale; + } + } + + /** Sets the locale for the current thread. + * + * @param locale Locale */ + public static void setThreadLocale(Locale locale) { + MAP_THREAD_TO_LOCALE.set(locale); + } + + /** Returns the preferred locale of the current thread, or null if the + * thread has not called {@link #setThreadLocale}. + * + * @return Locale */ + public static Locale getThreadLocale() { + return MAP_THREAD_TO_LOCALE.get(); + } + + /** Creates an instance of the resource object, using the class's name as + * the name of the resource file. + * + * @see #create(String, Class) + * + * @param <T> Resource type + * @param clazz Interface that contains a method for each resource + * @return Instance of the interface that can be used to instantiate + * resources + */ + public static <T> T create(Class<T> clazz) { + return create(clazz.getCanonicalName(), clazz); + } + + /** Creates an instance of the resource object. + * + * <p>The resource interface has methods that return {@link Inst} and + * {@link ExInst} values. Each of those methods is basically a factory method. + * + * <p>This method creates an instance of that interface backed by a resource + * bundle, using a dynamic proxy ({@link Proxy}). + * + * <p>Suppose that base = "com.example.MyResource" and the current locale is + * "en_US". A method + * + * <blockquote> + * @BaseMessage("Illegal binary string {0}") + * ExInst<IllegalArgumentException> illegalBinaryString(String a0); + * </blockquote> + * + * will look up a resource "IllegalBinaryString" from the resource file + * "com/example/MyResource_en_US.properties", and substitute in the parameter + * value {@code a0}. + * + * <p>The resource in the properties file may or may not be equal to the + * base message "Illegal binary string {0}". But in the base locale, it + * probably should be. + * + * @param <T> Resource type + * @param base Base name of the resource.properties file + * @param clazz Interface that contains a method for each resource + * @return Instance of the interface that can be used to instantiate + * resources + */ + public static <T> T create(String base, Class<T> clazz) { + return create(base, EmptyPropertyAccessor.INSTANCE, clazz); + } + + /** Creates an instance of the resource object that can access properties + * but not resources. */ + public static <T> T create(PropertyAccessor accessor, Class<T> clazz) { + return create(null, accessor, clazz); + } + + /** Creates an instance of the resource object that can access properties + * but not resources. */ + public static <T> T create(final Properties properties, Class<T> clazz) { + return create(null, new PropertiesAccessor(properties), clazz); + } + + private static <T> T create(final String base, + final PropertyAccessor accessor, Class<T> clazz) { + //noinspection unchecked + return (T) Proxy.newProxyInstance(clazz.getClassLoader(), + new Class[] {clazz}, + new InvocationHandler() { + final Map<String, Object> cache = + new ConcurrentHashMap<String, Object>(); + + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + if (args == null || args.length == 0) { + Object o = cache.get(method.getName()); + if (o == null) { + o = create(method, args); + cache.put(method.getName(), o); + } + return o; + } + return create(method, args); + } + + private Object create(Method method, Object[] args) + throws NoSuchMethodException, InstantiationException, + IllegalAccessException, InvocationTargetException { + if (method.equals(BuiltinMethod.OBJECT_TO_STRING.method)) { + return toString(); + } + final Class<?> returnType = method.getReturnType(); + try { + if (Inst.class.isAssignableFrom(returnType)) { + final Constructor<?> constructor = + returnType.getConstructor(String.class, Locale.class, + Method.class, Object[].class); + final Locale locale = Resources.getThreadOrDefaultLocale(); + return constructor.newInstance(base, locale, method, + args != null ? args : new Object[0]); + } else { + final Constructor<?> constructor = + returnType.getConstructor(PropertyAccessor.class, + Method.class); + return constructor.newInstance(accessor, method); + } + } catch (InvocationTargetException e) { + final Throwable e2 = e.getTargetException(); + if (e2 instanceof RuntimeException) { + throw (RuntimeException) e2; + } + if (e2 instanceof Error) { + throw (Error) e2; + } + throw e; + } + } + }); + } + + /** Applies all validations to all resource methods in the given + * resource object. + * + * @param o Resource object to validate + */ + public static void validate(Object o) { + validate(o, EnumSet.allOf(Validation.class)); + } + + /** Applies the given validations to all resource methods in the given + * resource object. + * + * @param o Resource object to validate + * @param validations Validations to perform + */ + public static void validate(Object o, EnumSet<Validation> validations) { + int count = 0; + for (Method method : o.getClass().getMethods()) { + if (!Modifier.isStatic(method.getModifiers()) + && Inst.class.isAssignableFrom(method.getReturnType())) { + ++count; + final Class<?>[] parameterTypes = method.getParameterTypes(); + Object[] args = new Object[parameterTypes.length]; + for (int i = 0; i < parameterTypes.length; i++) { + args[i] = zero(parameterTypes[i]); + } + try { + Inst inst = (Inst) method.invoke(o, args); + inst.validate(validations); + } catch (IllegalAccessException e) { + throw new RuntimeException("in " + method, e); + } catch (InvocationTargetException e) { + throw new RuntimeException("in " + method, e.getCause()); + } + } + } + if (count == 0 && validations.contains(Validation.AT_LEAST_ONE)) { + throw new AssertionError("resource object " + o + + " contains no resources"); + } + } + + private static Object zero(Class<?> clazz) { + return clazz == String.class ? "" + : clazz == byte.class ? (byte) 0 + : clazz == char.class ? (char) 0 + : clazz == short.class ? (short) 0 + : clazz == int.class ? 0 + : clazz == long.class ? 0L + : clazz == float.class ? 0F + : clazz == double.class ? 0D + : clazz == boolean.class ? false + : null; + } + + /** Returns whether two objects are equal or are both null. */ + private static boolean equal(Object o0, Object o1) { + return o0 == o1 || o0 != null && o0.equals(o1); + } + + /** Element in a resource (either a resource or a property). */ + public static class Element { + protected final Method method; + protected final String key; + + public Element(Method method) { + this.method = method; + this.key = deriveKey(); + } + + protected String deriveKey() { + final Resource resource = method.getAnnotation(Resource.class); + if (resource != null) { + return resource.value(); + } else { + final String name = method.getName(); + return Character.toUpperCase(name.charAt(0)) + name.substring(1); + } + } + } + + /** Resource instance. It contains the resource method (which + * serves to identify the resource), the locale with which we + * expect to render the resource, and any arguments. */ + public static class Inst extends Element { + private final Locale locale; + protected final String base; + protected final Object[] args; + + public Inst(String base, Locale locale, Method method, Object... args) { + super(method); + this.base = base; + this.locale = locale; + this.args = args; + } + + @Override public boolean equals(Object obj) { + return this == obj + || obj != null + && obj.getClass() == this.getClass() + && locale == ((Inst) obj).locale + && method == ((Inst) obj).method + && Arrays.equals(args, ((Inst) obj).args); + } + + @Override public int hashCode() { + return Arrays.asList(locale, method, Arrays.asList(args)).hashCode(); + } + + public ResourceBundle bundle() { + return ResourceBundle.getBundle(base, locale); + } + + public Inst localize(Locale locale) { + return new Inst(base, locale, method, args); + } + + public void validate(EnumSet<Validation> validations) { + final ResourceBundle bundle = bundle(); + for (Validation validation : validations) { + switch (validation) { + case BUNDLE_HAS_RESOURCE: + if (!bundle.containsKey(key)) { + throw new AssertionError("key '" + key + + "' not found for resource '" + method.getName() + + "' in bundle '" + bundle + "'"); + } + break; + case MESSAGE_SPECIFIED: + final BaseMessage annotation1 = + method.getAnnotation(BaseMessage.class); + if (annotation1 == null) { + throw new AssertionError("resource '" + method.getName() + + "' must specify BaseMessage"); + } + break; + case EVEN_QUOTES: + String message = method.getAnnotation(BaseMessage.class).value(); + if (countQuotesIn(message) % 2 == 1) { + throw new AssertionError("resource '" + method.getName() + + "' should have even number of quotes"); + } + break; + case MESSAGE_MATCH: + final BaseMessage annotation2 = + method.getAnnotation(BaseMessage.class); + if (annotation2 != null) { + final String value = annotation2.value(); + final String value2 = bundle.containsKey(key) + ? bundle.getString(key) + : null; + if (!equal(value, value2)) { + throw new AssertionError("message for resource '" + + method.getName() + + "' is different between class and resource file"); + } + } + break; + case ARGUMENT_MATCH: + String raw = raw(); + MessageFormat format = new MessageFormat(raw); + final Format[] formats = format.getFormatsByArgumentIndex(); + final List<Class> types = new ArrayList<Class>(); + final Class<?>[] parameterTypes = method.getParameterTypes(); + for (int i = 0; i < formats.length; i++) { + Format format1 = formats[i]; + Class parameterType = parameterTypes[i]; + final Class<?> e; + if (format1 instanceof NumberFormat) { + e = parameterType == short.class + || parameterType == int.class + || parameterType == long.class + || parameterType == float.class + || parameterType == double.class + || Number.class.isAssignableFrom(parameterType) + ? parameterType + : Number.class; + } else if (format1 instanceof DateFormat) { + e = Date.class; + } else { + e = String.class; + } + types.add(e); + } + final List<Class<?>> parameterTypeList = + Arrays.asList(parameterTypes); + if (!types.equals(parameterTypeList)) { + throw new AssertionError("type mismatch in method '" + + method.getName() + "' between message format elements " + + types + " and method parameters " + parameterTypeList); + } + break; + } + } + } + + private int countQuotesIn(String message) { + int count = 0; + for (int i = 0, n = message.length(); i < n; i++) { + if (message.charAt(i) == '\'') { + ++count; + } + } + return count; + } + + public String str() { + String message = raw(); + MessageFormat format = new MessageFormat(message); + format.setLocale(locale); + return format.format(args); + } + + public String raw() { + try { + return bundle().getString(key); + } catch (MissingResourceException e) { + // Resource is not in the bundle. (It is probably missing from the + // .properties file.) Fall back to the base message. + return method.getAnnotation(BaseMessage.class).value(); + } + } + + public Map<String, String> getProperties() { + // At present, annotations allow at most one property per resource. We + // could design new annotations if any resource needed more. + final Property property = method.getAnnotation(Property.class); + if (property == null) { + return Collections.emptyMap(); + } else { + return Collections.singletonMap(property.name(), property.value()); + } + } + } + + /** Sub-class of {@link Inst} that can throw an exception. Requires caused + * by exception.*/ + public static class ExInstWithCause<T extends Exception> extends Inst { + public ExInstWithCause(String base, Locale locale, Method method, + Object... args) { + super(base, locale, method, args); + } + + @Override public Inst localize(Locale locale) { + return new ExInstWithCause<T>(base, locale, method, args); + } + + public T ex(Throwable cause) { + try { + //noinspection unchecked + final Class<T> exceptionClass = + getExceptionClass(method.getGenericReturnType()); + Constructor<T> constructor; + final String str = str(); + boolean causeInConstructor = false; + try { + constructor = exceptionClass.getConstructor(String.class, + Throwable.class); + causeInConstructor = true; + } catch (NoSuchMethodException nsmStringThrowable) { + try { + constructor = exceptionClass.getConstructor(String.class); + } catch (NoSuchMethodException nsmString) { + // Ignore nsmString to encourage users to have (String, + // Throwable) constructors. + throw nsmStringThrowable; + } + } + if (causeInConstructor) { + return constructor.newInstance(str, cause); + } + T ex = constructor.newInstance(str); + if (cause != null) { + try { + ex.initCause(cause); + } catch (IllegalStateException iae) { + // Sorry, unable to add cause via constructor and via initCause + } + } + return ex; + } catch (InstantiationException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof Error) { + throw (Error) e.getCause(); + } else if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } else { + throw new RuntimeException(e); + } + } + } + + public static Class getExceptionClass(Type type) { + // Get exception type from ExInstWithCause<MyException> type parameter. + // ExInstWithCause might be one of super classes. + // And, this class may be a parameter-less sub-class of a generic base. + // + // NOTE: We used to use + // com.fasterxml.jackson.databind.type.TypeFactory.findTypeParameters. + // More powerful, but we can't afford an extra dependency. + + final Type type0 = type; + for (;;) { + if (type instanceof ParameterizedType) { + final Type[] types = + ((ParameterizedType) type).getActualTypeArguments(); + if (types.length >= 1 + && types[0] instanceof Class + && Throwable.class.isAssignableFrom((Class) types[0])) { + return (Class) types[0]; + } + throw new IllegalStateException( + "Unable to find superclass ExInstWithCause for " + type); + } + if (type instanceof Class) { + type = ((Class) type).getGenericSuperclass(); + if (type == null) { + throw new IllegalStateException( + "Unable to find superclass ExInstWithCause for " + type0); + } + } + } + } + + protected void validateException(Callable<Exception> exSupplier) { + Throwable cause = null; + try { + //noinspection ThrowableResultOfMethodCallIgnored + final Exception ex = exSupplier.call(); + if (ex == null) { + cause = new NullPointerException(); + } + } catch (AssertionError e) { + cause = e; + } catch (RuntimeException e) { + cause = e; + } catch (Exception e) { + // This can never happen since exSupplier should be just a ex() call. + // catch(Exception) is required since Callable#call throws Exception. + // Just in case we get exception somehow, we will rethrow it as a part + // of AssertionError below. + cause = e; + } + if (cause != null) { + AssertionError assertionError = new AssertionError( + "error instantiating exception for resource '" + + method.getName() + "'"); + assertionError.initCause(cause); + throw assertionError; + } + } + + @Override public void validate(EnumSet<Validation> validations) { + super.validate(validations); + if (validations.contains(Validation.CREATE_EXCEPTION)) { + validateException( + new Callable<Exception>() { + public Exception call() throws Exception { + return ex(new NullPointerException("test")); + } + }); + } + } + } + + /** Sub-class of {@link Inst} that can throw an exception without caused + * by. */ + public static class ExInst<T extends Exception> extends ExInstWithCause<T> { + public ExInst(String base, Locale locale, Method method, Object... args) { + super(base, locale, method, args); + } + + public T ex() { + return ex(null); + } + + @Override public void validate(EnumSet<Validation> validations) { + super.validate(validations); + if (validations.contains(Validation.CREATE_EXCEPTION)) { + validateException( + new Callable<Exception>() { + public Exception call() throws Exception { + return ex(); + } + }); + } + } + } + + /** Property instance. */ + public abstract static class Prop extends Element { + protected final PropertyAccessor accessor; + protected final boolean hasDefault; + + public Prop(PropertyAccessor accessor, Method method) { + super(method); + this.accessor = accessor; + final Default resource = method.getAnnotation(Default.class); + this.hasDefault = resource != null; + } + + public boolean isSet() { + return accessor.isSet(this); + } + + public boolean hasDefault() { + return hasDefault; + } + + void checkDefault() { + if (!hasDefault) { + throw new NoDefaultValueException("Property " + key + + " has no default value"); + } + } + + void checkDefault2() { + if (!hasDefault) { + throw new NoDefaultValueException("Property " + key + + " is not set and has no default value"); + } + } + } + + /** Integer property instance. */ + public static class IntProp extends Prop { + private final int defaultValue; + + public IntProp(PropertyAccessor accessor, Method method) { + super(accessor, method); + if (hasDefault) { + final Default resource = method.getAnnotation(Default.class); + defaultValue = Integer.parseInt(resource.value(), 10); + } else { + defaultValue = 0; + } + } + + /** Returns the value of this integer property. */ + public int get() { + return accessor.intValue(this); + } + + /** Returns the value of this integer property, returning the given default + * value if the property is not set. */ + public int get(int defaultValue) { + return accessor.intValue(this, defaultValue); + } + + public int defaultValue() { + checkDefault(); + return defaultValue; + } + } + + /** Boolean property instance. */ + public static class BooleanProp extends Prop { + private final boolean defaultValue; + + public BooleanProp(PropertyAccessor accessor, Method method) { + super(accessor, method); + if (hasDefault) { + final Default resource = method.getAnnotation(Default.class); + defaultValue = Boolean.parseBoolean(resource.value()); + } else { + defaultValue = false; + } + } + + /** Returns the value of this boolean property. */ + public boolean get() { + return accessor.booleanValue(this); + } + + /** Returns the value of this boolean property, returning the given default + * value if the property is not set. */ + public boolean get(boolean defaultValue) { + return accessor.booleanValue(this, defaultValue); + } + + public boolean defaultValue() { + checkDefault(); + return defaultValue; + } + } + + /** Double property instance. */ + public static class DoubleProp extends Prop { + private final double defaultValue; + + public DoubleProp(PropertyAccessor accessor, Method method) { + super(accessor, method); + if (hasDefault) { + final Default resource = method.getAnnotation(Default.class); + defaultValue = Double.parseDouble(resource.value()); + } else { + defaultValue = 0d; + } + } + + /** Returns the value of this double property. */ + public double get() { + return accessor.doubleValue(this); + } + + /** Returns the value of this double property, returning the given default + * value if the property is not set. */ + public double get(double defaultValue) { + return accessor.doubleValue(this, defaultValue); + } + + public double defaultValue() { + checkDefault(); + return defaultValue; + } + } + + /** String property instance. */ + public static class StringProp extends Prop { + private final String defaultValue; + + public StringProp(PropertyAccessor accessor, Method method) { + super(accessor, method); + if (hasDefault) { + final Default resource = method.getAnnotation(Default.class); + defaultValue = resource.value(); + } else { + defaultValue = null; + } + } + + /** Returns the value of this String property. */ + public String get() { + return accessor.stringValue(this); + } + + /** Returns the value of this String property, returning the given default + * value if the property is not set. */ + public String get(String defaultValue) { + return accessor.stringValue(this, defaultValue); + } + + public String defaultValue() { + checkDefault(); + return defaultValue; + } + } + + /** Thrown when a default value is needed but a property does not have + * one. */ + public static class NoDefaultValueException extends RuntimeException { + NoDefaultValueException(String message) { + super(message); + } + } + + /** Means by which a resource can get values of properties, given their + * name. */ + public interface PropertyAccessor { + boolean isSet(Prop p); + int intValue(IntProp p); + int intValue(IntProp p, int defaultValue); + String stringValue(StringProp p); + String stringValue(StringProp p, String defaultValue); + boolean booleanValue(BooleanProp p); + boolean booleanValue(BooleanProp p, boolean defaultValue); + double doubleValue(DoubleProp p); + double doubleValue(DoubleProp p, double defaultValue); + } + + enum EmptyPropertyAccessor implements PropertyAccessor { + INSTANCE; + + public boolean isSet(Prop p) { + return false; + } + + public int intValue(IntProp p) { + return p.defaultValue(); + } + + public int intValue(IntProp p, int defaultValue) { + return defaultValue; + } + + public String stringValue(StringProp p) { + return p.defaultValue(); + } + + public String stringValue(StringProp p, String defaultValue) { + return defaultValue; + } + + public boolean booleanValue(BooleanProp p) { + return p.defaultValue(); + } + + public boolean booleanValue(BooleanProp p, boolean defaultValue) { + return defaultValue; + } + + public double doubleValue(DoubleProp p) { + return p.defaultValue(); + } + + public double doubleValue(DoubleProp p, double defaultValue) { + return defaultValue; + } + } + + /** Types of validation that can be performed on a resource. */ + public enum Validation { + /** Checks that each method's resource key corresponds to a resource in the + * bundle. */ + BUNDLE_HAS_RESOURCE, + + /** Checks that there is at least one resource in the bundle. */ + AT_LEAST_ONE, + + /** Checks that the base message annotation is on every resource. */ + MESSAGE_SPECIFIED, + + /** Checks that every message contains even number of quotes. */ + EVEN_QUOTES, + + /** Checks that the base message matches the message in the bundle. */ + MESSAGE_MATCH, + + /** Checks that it is possible to create an exception. */ + CREATE_EXCEPTION, + + /** Checks that the parameters of the method are consistent with the + * format elements in the base message. */ + ARGUMENT_MATCH, + } + + /** The message in the default locale. */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface BaseMessage { + String value(); + } + + /** The name of the property in the resource file. */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface Resource { + String value(); + } + + /** Property of a resource. */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface Property { + String name(); + String value(); + } + + /** Default value of a property. */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface Default { + String value(); + } + + /** + * <code>ShadowResourceBundle</code> is an abstract base class for + * {@link ResourceBundle} classes which are backed by a properties file. When + * the class is created, it loads a properties file with the same name as the + * class. + * + * <p> In the standard scheme (see {@link ResourceBundle}), if + * you call <code>{@link ResourceBundle#getBundle}("foo.MyResource")</code>, + * it first looks for a class called <code>foo.MyResource</code>, then + * looks for a file called <code>foo/MyResource.properties</code>. If it finds + * the file, it creates a {@link PropertyResourceBundle} and loads the class. + * The problem is if you want to load the <code>.properties</code> file + * into a dedicated class; <code>ShadowResourceBundle</code> helps with this + * case. + * + * <p> You should create a class as follows:<blockquote> + * + * <pre>package foo; + *class MyResource extends ShadowResourceBundle { + * public MyResource() throws java.io.IOException { + * } + *}</pre> + * + * </blockquote> + * + * Then when you call + * {@link ResourceBundle#getBundle ResourceBundle.getBundle("foo.MyResource")}, + * it will find the class before the properties file, but still automatically + * load the properties file based upon the name of the class. + */ + public abstract static class ShadowResourceBundle extends ResourceBundle { + private PropertyResourceBundle bundle; + + /** + * Creates a <code>ShadowResourceBundle</code>, and reads resources from + * a <code>.properties</code> file with the same name as the current class. + * For example, if the class is called <code>foo.MyResource_en_US</code>, + * reads from <code>foo/MyResource_en_US.properties</code>, then + * <code>foo/MyResource_en.properties</code>, then + * <code>foo/MyResource.properties</code>. + * + * @throws IOException on error + */ + protected ShadowResourceBundle() throws IOException { + super(); + Class clazz = getClass(); + InputStream stream = openPropertiesFile(clazz); + if (stream == null) { + throw new IOException("could not open properties file for " + + getClass()); + } + MyPropertyResourceBundle previousBundle = + new MyPropertyResourceBundle(stream); + bundle = previousBundle; + stream.close(); + // Now load properties files for parent locales, which we deduce from + // the names of our super-class, and its super-class. + while (true) { + clazz = clazz.getSuperclass(); + if (clazz == null + || clazz == ShadowResourceBundle.class + || !ResourceBundle.class.isAssignableFrom(clazz)) { + break; + } + stream = openPropertiesFile(clazz); + if (stream == null) { + continue; + } + MyPropertyResourceBundle newBundle = + new MyPropertyResourceBundle(stream); + stream.close(); + previousBundle.setParentTrojan(newBundle); + previousBundle = newBundle; + } + } + + /** + * Opens the properties file corresponding to a given class. The code is + * copied from {@link ResourceBundle}. + */ + private static InputStream openPropertiesFile(Class clazz) { + final ClassLoader loader = clazz.getClassLoader(); + final String resName = clazz.getName().replace('.', '/') + ".properties"; + return (InputStream) java.security.AccessController.doPrivileged( + new java.security.PrivilegedAction() { + public Object run() { + if (loader != null) { + return loader.getResourceAsStream(resName); + } else { + return ClassLoader.getSystemResourceAsStream(resName); + } + } + }); + } + + public Enumeration<String> getKeys() { + return bundle.getKeys(); + } + + protected Object handleGetObject(String key) { + return bundle.getObject(key); + } + + /** + * Returns the instance of the <code>baseName</code> resource bundle + * for the given locale. + * + * <p> This method should be called from a derived class, with the proper + * casting:<blockquote> + * + * <pre>class MyResource extends ShadowResourceBundle { + * ... + * + * /** + * * Retrieves the instance of {@link MyResource} appropriate + * * to the given locale. + * **/ + * public static MyResource instance(Locale locale) { + * return (MyResource) instance( + * MyResource.class.getName(), locale, + * ResourceBundle.getBundle(MyResource.class.getName(), locale)); + * } + * ... + * }</pre></blockquote> + * + * @param baseName Base name + * @param locale Locale + * @param bundle Resource bundle + * @return Resource bundle + */ + protected static ShadowResourceBundle instance( + String baseName, Locale locale, ResourceBundle bundle) { + if (bundle instanceof PropertyResourceBundle) { + throw new ClassCastException( + "ShadowResourceBundle.instance('" + baseName + "','" + + locale + "') found " + + baseName + "_" + locale + ".properties but not " + + baseName + "_" + locale + ".class"); + } + return (ShadowResourceBundle) bundle; + } + } + + /** Resource bundle based on properties. */ + static class MyPropertyResourceBundle extends PropertyResourceBundle { + public MyPropertyResourceBundle(InputStream stream) throws IOException { + super(stream); + } + + void setParentTrojan(ResourceBundle parent) { + super.setParent(parent); + } + } + + enum BuiltinMethod { + OBJECT_TO_STRING(Object.class, "toString"); + + public final Method method; + + BuiltinMethod(Class clazz, String methodName, Class... argumentTypes) { + this.method = lookupMethod(clazz, methodName, argumentTypes); + } + + /** + * Finds a method of a given name that accepts a given set of arguments. + * Includes in its search inherited methods and methods with wider argument + * types. + * + * @param clazz Class against which method is invoked + * @param methodName Name of method + * @param argumentTypes Types of arguments + * + * @return A method with the given name that matches the arguments given + * @throws RuntimeException if method not found + */ + public static Method lookupMethod(Class clazz, String methodName, + Class... argumentTypes) { + try { + //noinspection unchecked + return clazz.getMethod(methodName, argumentTypes); + } catch (NoSuchMethodException e) { + throw new RuntimeException( + "while resolving method '" + methodName + + Arrays.toString(argumentTypes) + + "' in class " + clazz, e); + } + } + } + + /** Implementation of {@link PropertyAccessor} that reads from a + * {@link Properties}. */ + private static class PropertiesAccessor implements PropertyAccessor { + private final Properties properties; + + PropertiesAccessor(Properties properties) { + this.properties = properties; + } + + public boolean isSet(Prop p) { + return properties.containsKey(p.key); + } + + public int intValue(IntProp p) { + final String s = properties.getProperty(p.key); + if (s != null) { + return Integer.parseInt(s, 10); + } + p.checkDefault2(); + return p.defaultValue; + } + + public int intValue(IntProp p, int defaultValue) { + final String s = properties.getProperty(p.key); + return s == null ? defaultValue : Integer.parseInt(s, 10); + } + + public String stringValue(StringProp p) { + final String s = properties.getProperty(p.key); + if (s != null) { + return s; + } + p.checkDefault2(); + return p.defaultValue; + } + + public String stringValue(StringProp p, String defaultValue) { + final String s = properties.getProperty(p.key); + return s == null ? defaultValue : s; + } + + public boolean booleanValue(BooleanProp p) { + final String s = properties.getProperty(p.key); + if (s != null) { + return Boolean.parseBoolean(s); + } + p.checkDefault2(); + return p.defaultValue; + } + + public boolean booleanValue(BooleanProp p, boolean defaultValue) { + final String s = properties.getProperty(p.key); + return s == null ? defaultValue : Boolean.parseBoolean(s); + } + + public double doubleValue(DoubleProp p) { + final String s = properties.getProperty(p.key); + if (s != null) { + return Double.parseDouble(s); + } + p.checkDefault2(); + return p.defaultValue; + } + + public double doubleValue(DoubleProp p, double defaultValue) { + final String s = properties.getProperty(p.key); + return s == null ? defaultValue : Double.parseDouble(s); + } + } +} + +// End Resources.java
