/**
* Copyright 2015 Netflix, Inc.
*
* Licensed 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.netflix.spectator.impl;
import com.netflix.spectator.api.RegistryConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Constructor;
import java.lang.reflect.Proxy;
import java.time.Duration;
import java.time.Instant;
import java.time.Period;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.function.Function;
/**
* Helper methods for accessing configuration settings.
*
* <p><b>This class is an internal implementation detail only intended for use within spectator.
* It is subject to change without notice.</b></p>
*/
public final class Config {
private static final Logger LOGGER = LoggerFactory.getLogger(Config.class);
private static final String PREFIX = "spectator.api.";
private static final Map<Class<?>, Function<String, Object>> PARSERS = new HashMap<>();
static {
PARSERS.put(Boolean.class, Boolean::valueOf);
PARSERS.put(Boolean.TYPE, Boolean::valueOf);
PARSERS.put(Byte.class, Byte::valueOf);
PARSERS.put(Byte.TYPE, Byte::valueOf);
PARSERS.put(Short.class, Short::valueOf);
PARSERS.put(Short.TYPE, Short::valueOf);
PARSERS.put(Integer.class, Integer::valueOf);
PARSERS.put(Integer.TYPE, Integer::valueOf);
PARSERS.put(Long.class, Long::valueOf);
PARSERS.put(Long.TYPE, Long::valueOf);
PARSERS.put(Float.class, Float::valueOf);
PARSERS.put(Float.TYPE, Float::valueOf);
PARSERS.put(Double.class, Double::valueOf);
PARSERS.put(Double.TYPE, Double::valueOf);
PARSERS.put(Character.class, s -> s.charAt(0));
PARSERS.put(Character.TYPE, s -> s.charAt(0));
PARSERS.put(String.class, s -> s);
PARSERS.put(Duration.class, Duration::parse);
PARSERS.put(Period.class, Period::parse);
PARSERS.put(Instant.class, Instant::parse);
PARSERS.put(ZonedDateTime.class, ZonedDateTime::parse);
PARSERS.put(ZoneId.class, ZoneId::of);
}
private static final RegistryConfig DEFAULT_CONFIG = usingSystemProperties("spectator.api.");
private Config() {
}
@SuppressWarnings("PMD.UseProperClassLoader")
private static ClassLoader classLoader() {
final ClassLoader cl = Thread.currentThread().getContextClassLoader();
return (cl == null) ? Config.class.getClassLoader() : cl;
}
private static Object valueOf(Class<?> to, String value) {
return PARSERS.get(to).apply(value);
}
private static Constructor<MethodHandles.Lookup> getConstructor() {
// https://rmannibucau.wordpress.com/2014/03/27/java-8-default-interface-methods-and-jdk-dynamic-proxies/
try {
final Constructor<MethodHandles.Lookup> constructor =
MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class);
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
return constructor;
} catch (Exception e) {
LOGGER.error("failed to make MethodHandles.Lookup accessible, config proxy may not work", e);
throw new RuntimeException(e);
}
}
/**
* Create a proxy class that implements the inferface specified.
*
* @param cls
* Interface that represents configuration settings.
* @param props
* A function that maps a string key to a string value. This is the source of configuration
* settings. It is expected that the function is thread safe. The key used to lookup a
* value is the method name that is invoked on the interface.
* @return
* Instance of the interface that maps methods to the corresponding key in the {@code props}
* function.
*/
@SuppressWarnings("unchecked")
public static <T> T createProxy(Class<T> cls, Function<String, String> props) {
final Constructor<MethodHandles.Lookup> constructor = getConstructor();
final Class<?>[] interfaces = new Class<?>[] {cls};
return (T) Proxy.newProxyInstance(classLoader(), interfaces, (proxy, method, args) -> {
final String name = method.getName();
if (method.isDefault()) {
final Class<?> declaringClass = method.getDeclaringClass();
return constructor.newInstance(declaringClass, MethodHandles.Lookup.PRIVATE)
.unreflectSpecial(method, declaringClass)
.bindTo(proxy)
.invokeWithArguments(args);
} else if ("get".equals(method.getName())) {
return props.apply((String) args[0]);
} else {
Class<?> rt = method.getReturnType();
String v = props.apply(name);
if (v == null) {
throw new NoSuchElementException("could not find value for config setting: " + name);
}
return valueOf(rt, v);
}
});
}
/**
* Create a proxy class that implements the inferface specified.
*
* @param cls
* Interface that represents configuration settings.
* @param props
* Map storing the properties. It is expected that the map implementation is thread safe.
* The key used to lookup a value is the method name that is invoked on the interface.
* @return
* Instance of the interface that maps methods to the corresponding key in the {@code props}
* map.
*/
public static <T> T usingMap(Class<T> cls, Map<String, String> props) {
return createProxy(cls, props::get);
}
/**
* Create a proxy class that implements the inferface specified.
*
* @param cls
* Interface that represents configuration settings.
* @param props
* Properties object storing the settings. The key used to lookup a value is the method name
* that is invoked on the interface.
* @param prefix
* Prefix applied to the key before looking it up in the properties object.
* @return
* Instance of the interface that maps methods to the corresponding key in the {@code props}
* object.
*/
public static <T> T usingProperties(Class<T> cls, Properties props, String prefix) {
return createProxy(cls, k -> props.getProperty(prefix + k));
}
/**
* Create a proxy class that implements the inferface specified. Data will come from
* {@link System#getProperties()}.
*
* @param cls
* Interface that represents configuration settings.
* @param prefix
* Prefix applied to the key before looking it up in the properties object.
* @return
* Instance of the interface that maps methods to the corresponding key in the {@code props}
* object.
*/
public static <T> T usingSystemProperties(Class<T> cls, String prefix) {
return createProxy(cls, k -> System.getProperty(prefix + k));
}
/**
* Create an instance of RegistryConfig backed by map. The map implementation should be
* thread-safe or not get modified after the config instance is created.
*
* @return
* Instance of RegistryConfig.
*/
public static RegistryConfig usingMap(Map<String, String> props) {
return usingMap(RegistryConfig.class, props);
}
/**
* Create an instance of RegistryConfig backed by the provided properties object.
*
* @param prefix
* Prefix applied to the key before looking it up in the properties object.
* @return
* Instance of RegistryConfig.
*/
public static RegistryConfig usingProperties(Properties props, String prefix) {
return usingProperties(RegistryConfig.class, props, prefix);
}
/**
* Create an instance of RegistryConfig backed by system properties.
*
* @param prefix
* Prefix applied to the key before looking it up in the properties object.
* @return
* Instance of RegistryConfig.
*/
public static RegistryConfig usingSystemProperties(String prefix) {
return usingSystemProperties(RegistryConfig.class, prefix);
}
/**
* Returns a default implementation of the registry config backed by system properties.
*/
public static RegistryConfig defaultConfig() {
return DEFAULT_CONFIG;
}
private static String get(String k) {
return System.getProperty(k);
}
private static String get(String k, String dflt) {
final String v = get(k);
return (v == null) ? dflt : v;
}
/** Should an exception be thrown for warnings? */
public static boolean propagateWarnings() {
return Boolean.valueOf(get(PREFIX + "propagateWarnings", "false"));
}
/**
* For classes based on {@link com.netflix.spectator.api.AbstractRegistry} this setting is used
* to determine the maximum number of registered meters permitted. This limit is used to help
* protect the system from a memory leak if there is a bug or irresponsible usage of registering
* meters.
*
* @return
* Maximum number of distinct meters that can be registered at a given time. The default is
* {@link java.lang.Integer#MAX_VALUE}.
*/
public static int maxNumberOfMeters() {
final String v = get(PREFIX + "maxNumberOfMeters");
return (v == null) ? Integer.MAX_VALUE : Integer.parseInt(v);
}
}