/*
* Copyright 2016 DiffPlug
*
* 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.diffplug.common.base;
import java.util.concurrent.ConcurrentHashMap;
/**
* A programmatic and pluggable implementation of <code>public static final</code>.
* <p>
* To write code that requires an instance of <code>IPlugin</code>, you write something like this:
* <p>
* <code>IPlugin instance = DurianPlugins.get(IPlugin.class, defaultImplementation);</code>
* <p>
* Once someone has requested a class from DurianPlugins, whatever instance was
* returned will continue to be returned for every future call. This has the same
* behavior as if you declared a <code>public static final IPlugin</code>.
* <p>
* Before the value has been requested, it can be set programmatically using {@link #register},
* or by setting a system property. This allows setting global behaviors appropriate
* for diverse environments.
* <p>
* This eternal instance for a given class is determined by:
* <ol>
* <li> the first call to {@link #register}
* <li> if a system property named <code>durian.plugins.{pluginClass.getCanonicalName()}</code> is set to
* the fully-qualified name (<code>Class.getName()</code>) of an implementation class with a no-argument
* constructor, then an instance of that class will be instantiated and used as the plugin implementation
* <li> the <code>defaultImplementation</code> that was specified in the first call to get()
* </ol>
* <p>
* This class was inspired by <a href="https://github.com/ReactiveX/RxJava/blob/86147542573004f4df84d2a2de83577cf62fe787/src/main/java/rx/plugins/RxJavaPlugins.java">RxJava's RxJavaPlugins</a>. Many thanks to them!
* @see <a href="https://diffplug.github.io/durian/javadoc/snapshot/com/diffplug/common/base/Errors.Plugins.OnErrorThrowAssertion.html">Errors.Plugins.OnErrorThrowAssertion</a>
* @see <a href="https://diffplug.github.io/durian-rx/javadoc/snapshot/com/diffplug/common/rx/RxTracingPolicy.LogSubscriptionTrace.html">RxTracingPolicy.LogSubscriptionTrace</a>
*/
public class DurianPlugins {
/** Resets the plugin instance for testing. */
static void resetForTesting() {
INSTANCE = new DurianPlugins();
}
private DurianPlugins() {}
private static DurianPlugins INSTANCE = new DurianPlugins();
/** Map which supports atomic putIfAbsent() and computeIfAbsent(). */
private final ConcurrentHashMap<Class<?>, Object> map = new ConcurrentHashMap<>();
/**
* Registers an implementation as a global override of any injected or default implementations.
*
* @param pluginClass The interface which is being registered.
* @param pluginImpl The implementation of that interface which is being registered.
* @throws IllegalStateException If called more than once or if a value has already been requested.
*/
public static <T> void register(Class<T> pluginClass, T pluginImpl) throws IllegalStateException {
INSTANCE.registerInternal(pluginClass, pluginImpl);
}
private <T> void registerInternal(Class<T> pluginClass, T pluginImpl) throws IllegalStateException {
assert (pluginClass.isInstance(pluginImpl));
Object existingValue = map.putIfAbsent(pluginClass, pluginImpl);
if (existingValue != null) {
throw new IllegalStateException("Another " + pluginClass + " was already registered: " + existingValue);
}
}
/**
* Returns an instance of pluginClass which is guaranteed to be identical throughout
* the runtime existence of this library. The returned instance is determined by:
* <ol>
* <li> the first call to {@link #register}
* <li> if a system property named <code>durian.plugins.{pluginClass.getCanonicalName()}</code> is set to
* the fully-qualified name (<code>Class.getName()</code>) of an implementation class with a no-argument
* constructor, then an instance of that class will be instantiated and used as the plugin implementation
* <li> the <code>defaultImplementation</code> that was specified in the first call to get()
* </ol>
* @param pluginClass The interface which is being requested.
* @param defaultImpl A default implementation of that interface.
* @return An instance of pluginClass, which is guaranteed to be returned for every future request for pluginClass.
*/
public static <T> T get(Class<T> pluginClass, T defaultImpl) {
return INSTANCE.getInternal(pluginClass, defaultImpl);
}
@SuppressWarnings("unchecked")
private <T> T getInternal(Class<T> pluginClass, T defaultImpl) {
assert (pluginClass.isInstance(defaultImpl));
return (T) map.computeIfAbsent(pluginClass, clazz -> {
// check for an implementation from System.getProperty first
Object impl = getPluginImplementationViaProperty(clazz);
return impl != null ? impl : defaultImpl;
});
}
/** Returns an implementation of the given class using the system properties as a registry. */
private static Object getPluginImplementationViaProperty(Class<?> pluginClass) {
String className = pluginClass.getCanonicalName();
if (className == null) {
throw new IllegalArgumentException("Class " + pluginClass + " does not have a canonical name!");
}
// Check system properties for plugin class.
// This will only happen during system startup thus it's okay to use the synchronized
// System.getProperties as it will never get called in normal operations.
String implementingClass = System.getProperty(PROPERTY_PREFIX + className);
if (implementingClass != null) {
try {
Class<?> cls = Class.forName(implementingClass);
// narrow the scope (cast) to the type we're expecting
cls = cls.asSubclass(pluginClass);
return cls.newInstance();
} catch (ClassCastException e) {
throw new RuntimeException(className + " implementation is not an instance of " + className + ": " + implementingClass);
} catch (ClassNotFoundException e) {
throw new RuntimeException(className + " implementation class not found: " + implementingClass, e);
} catch (InstantiationException e) {
throw new RuntimeException(className + " implementation not able to be instantiated: " + implementingClass, e);
} catch (IllegalAccessException e) {
throw new RuntimeException(className + " implementation not able to be accessed: " + implementingClass, e);
}
} else {
return null;
}
}
/** Prefix to system property keys for specifying plugin classes. */
public static final String PROPERTY_PREFIX = "durian.plugins.";
}