com.hazelcast.shaded.org.apache.calcite.runtime.Resources Maven / Gradle / Ivy
/*
* 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 com.hazelcast.shaded.org.apache.calcite.runtime;
import com.hazelcast.shaded.org.checkerframework.checker.initialization.qual.UnderInitialization;
import com.hazelcast.shaded.org.checkerframework.checker.nullness.qual.Nullable;
import com.hazelcast.shaded.org.checkerframework.checker.nullness.qual.PolyNull;
import com.hazelcast.shaded.org.checkerframework.checker.nullness.qual.RequiresNonNull;
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.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.text.DateFormat;
import java.text.Format;
import java.text.MessageFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Properties;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import static com.hazelcast.shaded.org.apache.calcite.linq4j.Nullness.castNonNull;
import static java.util.Objects.requireNonNull;
/**
* Defining wrapper classes around resources that allow the compiler to check
* whether the resources exist, and that uses of resources have the appropriate
* number and types of arguments to match the message.
*/
public class Resources {
private static final ThreadLocal<@Nullable Locale> MAP_THREAD_TO_LOCALE =
new ThreadLocal<>();
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 @Nullable 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 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 create(Class clazz) {
return create(clazz.getCanonicalName(), clazz);
}
/** Creates an instance of the resource object.
*
* The resource interface has methods that return {@link Inst} and
* {@link ExInst} values. Each of those methods is basically a factory method.
*
*
This method creates an instance of that interface backed by a resource
* bundle, using a dynamic proxy ({@link Proxy}).
*
*
Suppose that base = "com.example.MyResource" and the current locale is
* "en_US". A method
*
*
* @BaseMessage("Illegal binary string {0}")
* ExInst<IllegalArgumentException> illegalBinaryString(String a0);
*
*
* will look up a resource "IllegalBinaryString" from the resource file
* "com/example/MyResource_en_US.properties", and substitute in the parameter
* value {@code a0}.
*
*
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 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 create(@Nullable String base, Class clazz) {
return create(base, EmptyPropertyAccessor.INSTANCE, clazz);
}
/** Creates an instance of the resource object that can access properties
* but not resources. */
public static T create(PropertyAccessor accessor, Class clazz) {
return create(null, accessor, clazz);
}
/** Creates an instance of the resource object that can access properties
* but not resources. */
public static T create(final Properties properties, Class clazz) {
return create(null, new PropertiesAccessor(properties), clazz);
}
private static T create(final @Nullable String base,
final PropertyAccessor accessor, Class clazz) {
//noinspection unchecked
return (T) Proxy.newProxyInstance(clazz.getClassLoader(),
new Class[] {clazz},
new InvocationHandler() {
final Map cache = new ConcurrentHashMap<>();
@Override public Object invoke(Object proxy, Method method, @Nullable Object @Nullable [] 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, @Nullable Object @Nullable [] 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 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();
@Nullable 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);
assert inst != null : "got null from " + method;
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 @Nullable 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(@Nullable Object o0, @Nullable 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;
@SuppressWarnings("method.invocation.invalid")
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 @Nullable Object[] args;
public Inst(String base, Locale locale, Method method, @Nullable Object... args) {
super(method);
this.base = base;
this.locale = locale;
this.args = args;
}
@Override public boolean equals(@Nullable 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 validations) {
final ResourceBundle bundle = bundle();
for (Validation validation : validations) {
switch (validation) {
case BUNDLE_HAS_RESOURCE:
if (!bundle.containsKey(key)) {
String suggested = null;
final BaseMessage annotation =
method.getAnnotation(BaseMessage.class);
if (annotation != null) {
final String message = annotation.value();
suggested = "; add the following line to "
+ bundle.getBaseBundleName() + ".properties:\n"
+ key + '=' + message + "\n";
}
throw new AssertionError("key '" + key
+ "' not found for resource '" + method.getName()
+ "' in bundle '" + bundle + "'"
+ (suggested == null ? "" : suggested));
}
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 = requireNonNull(
method.getAnnotation(BaseMessage.class),
() -> "@BaseMessage is missing for resource '" + method.getName() + "'").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 @Nullable Format[] formats = format.getFormatsByArgumentIndex();
final List types = new ArrayList<>();
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> 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;
default:
break;
}
}
}
private static 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 requireNonNull(
method.getAnnotation(BaseMessage.class),
() -> "@BaseMessage is missing for resource '" + method.getName() + "'").value();
}
}
public Map 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 extends Inst {
public ExInstWithCause(String base, Locale locale, Method method,
@Nullable Object... args) {
super(base, locale, method, args);
}
@Override public Inst localize(Locale locale) {
return new ExInstWithCause(base, locale, method, args);
}
public T ex(@Nullable Throwable cause) {
try {
//noinspection unchecked
final Class exceptionClass =
getExceptionClass(method.getGenericReturnType());
Constructor 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 | IllegalAccessException
| 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 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 superclass = ((Class) type).getGenericSuperclass();
if (superclass == null) {
throw new IllegalStateException(
"Unable to find superclass ExInstWithCause for " + type0);
}
type = superclass;
}
}
}
protected void validateException(Callable extends @Nullable Exception> exSupplier) {
Throwable cause = null;
try {
//noinspection ThrowableResultOfMethodCallIgnored
final Exception ex = exSupplier.call();
if (ex == null) {
cause = new NullPointerException();
}
} catch (AssertionError | Exception e) {
// catch(Exception) is required since Callable#call throws Exception.
// But in practice, e will always be AssertionError or RuntimeException,
// since exSupplier should be just a ex() call.
cause = e;
}
if (cause != null) {
throw new AssertionError("error instantiating exception for resource '"
+ method.getName() + "'", cause);
}
}
@Override public void validate(EnumSet validations) {
super.validate(validations);
if (validations.contains(Validation.CREATE_EXCEPTION)) {
validateException(() -> ex(new NullPointerException("test")));
}
}
}
/** Sub-class of {@link Inst} that can throw an exception without caused
* by. */
public static class ExInst extends ExInstWithCause {
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 validations) {
super.validate(validations);
if (validations.contains(Validation.CREATE_EXCEPTION)) {
validateException(this::ex);
}
}
}
/** Property instance. */
public abstract static class Prop extends Element {
protected final PropertyAccessor accessor;
protected final boolean hasDefault;
protected Prop(PropertyAccessor accessor, Method method) {
super(method);
this.accessor = accessor;
final Default resource = method.getAnnotation(Default.class);
this.hasDefault = resource != null;
}
@RequiresNonNull("method")
protected final @Nullable Default getDefault(
@UnderInitialization Prop this
) {
if (hasDefault) {
return castNonNull(method.getAnnotation(Default.class));
} else {
return 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);
final Default resource = getDefault();
if (resource != null) {
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);
final Default resource = getDefault();
if (resource != null) {
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);
final Default resource = getDefault();
if (resource != null) {
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 @Nullable String defaultValue;
public StringProp(PropertyAccessor accessor, Method method) {
super(accessor, method);
final Default resource = getDefault();
if (resource != null) {
defaultValue = resource.value();
} else {
defaultValue = null;
}
}
/** Returns the value of this String property. */
public @Nullable String get() {
return accessor.stringValue(this);
}
/** Returns the value of this String property, returning the given default
* value if the property is not set.
*
* If {@code defaultValue} is not null, never returns null. */
public @PolyNull String get(@PolyNull String defaultValue) {
return accessor.stringValue(this, defaultValue);
}
public @Nullable 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);
@Nullable String stringValue(StringProp p);
@PolyNull String stringValue(StringProp p, @PolyNull 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;
@Override
public boolean isSet(Prop p) {
return false;
}
@Override
public int intValue(IntProp p) {
return p.defaultValue();
}
@Override
public int intValue(IntProp p, int defaultValue) {
return defaultValue;
}
@Override public @Nullable String stringValue(StringProp p) {
return p.defaultValue();
}
@Override public @PolyNull String stringValue(StringProp p,
@PolyNull String defaultValue) {
return defaultValue;
}
@Override
public boolean booleanValue(BooleanProp p) {
return p.defaultValue();
}
@Override
public boolean booleanValue(BooleanProp p, boolean defaultValue) {
return defaultValue;
}
@Override
public double doubleValue(DoubleProp p) {
return p.defaultValue();
}
@Override
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();
}
/**
* ShadowResourceBundle
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.
*
*
In the standard scheme (see {@link ResourceBundle}), if
* you call {@link ResourceBundle#getBundle}("foo.MyResource")
,
* it first looks for a class called foo.MyResource
, then
* looks for a file called foo/MyResource.properties
. If it finds
* the file, it creates a {@link PropertyResourceBundle} and loads the class.
* The problem is if you want to load the .properties
file
* into a dedicated class; ShadowResourceBundle
helps with this
* case.
*
*
You should create a class as follows:
*
*
* package foo;
*
* class MyResource extends ShadowResourceBundle {
* public MyResource() throws java.io.IOException {
* }
* }
*
*
* 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 final PropertyResourceBundle bundle;
/**
* Creates a ShadowResourceBundle
, and reads resources from
* a .properties
file with the same name as the current class.
* For example, if the class is called foo.MyResource_en_US
,
* reads from foo/MyResource_en_US.properties
, then
* foo/MyResource_en.properties
, then
* foo/MyResource.properties
.
*
* @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}.
*/
@SuppressWarnings("removal")
private static @Nullable InputStream openPropertiesFile(Class clazz) {
final ClassLoader loader = clazz.getClassLoader();
final String resName = clazz.getName().replace('.', '/') + ".properties";
return java.security.AccessController.doPrivileged(
(java.security.PrivilegedAction<@Nullable InputStream>) () -> {
if (loader != null) {
return loader.getResourceAsStream(resName);
} else {
return ClassLoader.getSystemResourceAsStream(resName);
}
});
}
@Override
public Enumeration getKeys() {
return bundle.getKeys();
}
@Override
protected Object handleGetObject(String key) {
return bundle.getObject(key);
}
/**
* Returns the instance of the baseName
resource bundle
* for the given locale.
*
* This method should be called from a derived class, with the proper
* casting:
*
*
* 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));
* }
* ...
* }
*
*
* @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");
@SuppressWarnings("ImmutableEnumChecker")
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;
}
@Override
public boolean isSet(Prop p) {
return properties.containsKey(p.key);
}
@Override
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;
}
@Override
public int intValue(IntProp p, int defaultValue) {
final String s = properties.getProperty(p.key);
return s == null ? defaultValue : Integer.parseInt(s, 10);
}
@Override public @Nullable String stringValue(StringProp p) {
final String s = properties.getProperty(p.key);
if (s != null) {
return s;
}
p.checkDefault2();
return p.defaultValue;
}
@Override public @PolyNull String stringValue(StringProp p,
@PolyNull String defaultValue) {
final String s = properties.getProperty(p.key);
return s == null ? defaultValue : s;
}
@Override
public boolean booleanValue(BooleanProp p) {
final String s = properties.getProperty(p.key);
if (s != null) {
return Boolean.parseBoolean(s);
}
p.checkDefault2();
return p.defaultValue;
}
@Override
public boolean booleanValue(BooleanProp p, boolean defaultValue) {
final String s = properties.getProperty(p.key);
return s == null ? defaultValue : Boolean.parseBoolean(s);
}
@Override
public double doubleValue(DoubleProp p) {
final String s = properties.getProperty(p.key);
if (s != null) {
return Double.parseDouble(s);
}
p.checkDefault2();
return p.defaultValue;
}
@Override
public double doubleValue(DoubleProp p, double defaultValue) {
final String s = properties.getProperty(p.key);
return s == null ? defaultValue : Double.parseDouble(s);
}
}
}