/** Resets the plugin instance for testing. */ static void resetForTesting() { INSTANCE = new DurianPlugins(); }
/** * Returns an instance of pluginClass which is guaranteed to be identical throughout * the runtime existence of this library. The returned instance is determined by: * * 1. the first call to {@link #register} * 2. if a system property named `durian.plugins.{pluginClass.getCanonicalName()}` is set to * the fully-qualified name (`Class.getName()`) of an implementation class with a no-argument * constructor, then an instance of that class will be instantiated and used as the plugin implementation * 3. the `defaultImplementation` that was specified in the first call to get() * * @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; }); }
/** * Opens a dialog to notify the user of any exceptions. It should be used in cases where * an error is too severe to be silently logged. * <p> * By default, dialog() opens a JOptionPane. To modify this behavior in your application, * call DurianPlugins.set(Errors.Plugins.Dialog.class, error -> openMyDialog(error)); * <p> * For a non-interactive console application, a good implementation of would probably * print the error and call System.exit(). * * @see DurianPlugins * @see Errors.Plugins.OnErrorThrowAssertion */ @SuppressFBWarnings(value = "LI_LAZY_INIT_STATIC", justification = "This race condition is fine, as explained in the comment below.") public static Handling dialog() { if (dialog == null) { // There is an acceptable race condition here. See Errors.log() for details. dialog = createHandling(DurianPlugins.get(Plugins.Dialog.class, Plugins::defaultDialog)); } return dialog; }
@Test(expected = IllegalStateException.class) public void testRegisterTooLate() { TestLogHandler logHandler = new TestLogHandler(); // set the value using a default value Assert.assertEquals(logHandler, DurianPlugins.get(Errors.Plugins.Log.class, logHandler)); // try to set the value using register (it should throw an exception) DurianPlugins.register(Errors.Plugins.Log.class, logHandler); }
/** * 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); }
@Test public void testWiresCrossed() { DurianPlugins.resetForTesting(); Errors.resetForTesting(); DurianPlugins.register(Errors.Plugins.Log.class, new TestHandler("Log")); DurianPlugins.register(Errors.Plugins.Dialog.class, new TestHandler("Dialog")); try { Errors.suppress().run(ErrorsTest::throwException); try { Errors.rethrow().run(ErrorsTest::throwException); } catch (RuntimeException e) { // it should pass the RuntimeException unphased Assert.assertNull(e.getCause()); } try { Errors.log().run(ErrorsTest::throwException); } catch (RuntimeException e) { Assert.assertEquals("Log", e.getMessage()); } try { Errors.dialog().run(ErrorsTest::throwException); } catch (RuntimeException e) { Assert.assertEquals("Dialog", e.getMessage()); } } finally { DurianPlugins.resetForTesting(); Errors.resetForTesting(); } }
/** Create a fresh instance of DurianPlugins for each test, and save the system properties that we're gonna futz with. */ @Before public void before() { DurianPlugins.resetForTesting(); for (Class<?> plugin : pluginsToStore) { String key = DurianPlugins.PROPERTY_PREFIX + plugin.getCanonicalName(); // store the current value Optional<String> value = Optional.ofNullable(System.getProperty(key)); state.put(plugin, value); // then clear the property System.clearProperty(key); } }
public void whereDoLogAndDialogComeFrom() { // Errors.log() promises to "log", and Errors.dialog() promises to alert the user. // Doing those things is very different depending on whether your code is running as a web // application, console application, or desktop gui // One way around this ambiguity is to avoid using Errors.log() and Errors.dialog() // entirely, and only use your own custom Errors. But if you are shipping a framework, // and your users might end up using Errors.log() and .dialog(), then you might want to // specify what those do. // By default, Errors.log() is just Throwable.printStackTrace, and Errors.dialog() // opens a JOptionPane. You can modify this behavior with the following: DurianPlugins.register(Errors.Plugins.Log.class, error -> { // log to twitter }); DurianPlugins.register(Errors.Plugins.Dialog.class, error -> { // Headless application: email the sysadmin and exit // Web application: ajax an alert() to the user }); // The trick is, you have to call these methods BEFORE Errors.log() or Errors.dialog() // are used anywhere in your whole application. Once log() or dialog() have been used, they are // fixed for the duration of the runtime. If you're writing a library, then you probably shouldn't // try to change them. If you're writing an application or framework, then you probably should. // If you're running in a JUnit environment, then you probably want any call to log() or dialog() // to kill the test. Setting the following system properties (again, before log() or dialog() are // called) will cause any errors to get wrapped and thrown as a java.lang.AssertionError. System.setProperty("durian.plugins.com.diffplug.common.base.Errors.Plugins.Log", "com.diffplug.common.base.Errors$Plugins$OnErrorThrowAssertion"); System.setProperty("durian.plugins.com.diffplug.common.base.Errors.Plugins.Dialog", "com.diffplug.common.base.Errors$Plugins$OnErrorThrowAssertion"); }
/** * Opens a dialog to notify the user of any exceptions. It should be used in cases where * an error is too severe to be silently logged. * <p> * By default, dialog() opens a JOptionPane. To modify this behavior in your application, * call DurianPlugins.set(Errors.Plugins.Dialog.class, error -> openMyDialog(error)); * <p> * For a non-interactive console application, a good implementation of would probably * print the error and call System.exit(). * * @see DurianPlugins * @see Errors.Plugins.OnErrorThrowAssertion */ @SuppressFBWarnings(value = "LI_LAZY_INIT_STATIC", justification = "This race condition is fine, as explained in the comment below.") public static Handling dialog() { if (dialog == null) { // There is an acceptable race condition here. See Errors.log() for details. dialog = createHandling(DurianPlugins.get(Plugins.Dialog.class, Plugins::defaultDialog)); } return dialog; }
@Test public void testRegister() { // the instance that will be set as a default TestLogHandler logHandler = new TestLogHandler(); // set the plugin as the default DurianPlugins.register(Errors.Plugins.Log.class, logHandler); // make sure that it returns the value we set, and not the default value Assert.assertEquals(logHandler, DurianPlugins.get(Errors.Plugins.Log.class, new TheWrongLogHandler())); }
/** * 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); }
/** Restore the system properties. */ @After public void after() { for (Class<?> plugin : pluginsToStore) { String key = DurianPlugins.PROPERTY_PREFIX + plugin.getCanonicalName(); // restore the stored value Optional<String> value = state.get(plugin); if (value.isPresent()) { System.setProperty(key, value.get()); } else { System.clearProperty(key); } } DurianPlugins.resetForTesting(); }
/** * Opens a dialog to notify the user of any exceptions. It should be used in cases where * an error is too severe to be silently logged. * <p> * By default, dialog() opens a JOptionPane. To modify this behavior in your application, * call DurianPlugins.set(Errors.Plugins.Dialog.class, error -> openMyDialog(error)); * <p> * For a non-interactive console application, a good implementation of would probably * print the error and call System.exit(). * * @see DurianPlugins * @see Errors.Plugins.OnErrorThrowAssertion */ @SuppressFBWarnings(value = "LI_LAZY_INIT_STATIC", justification = "This race condition is fine, as explained in the comment below.") public static Handling dialog() { if (dialog == null) { // There is an acceptable race condition here. See Errors.log() for details. dialog = createHandling(DurianPlugins.get(Plugins.Dialog.class, Plugins::defaultDialog)); } return dialog; }
@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; }); }
/** * 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); }
/** Resets the plugin instance for testing. */ static void resetForTesting() { INSTANCE = new DurianPlugins(); }
/** * 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); }
@Test(expected = AssertionError.class) public void testAssertionPlugin() { DurianPlugins.resetForTesting(); Errors.resetForTesting(); try { // set the Log handler to the be the OnErrorThrowAssertion System.setProperty("durian.plugins.com.diffplug.common.base.Errors.Plugins.Log", "com.diffplug.common.base.Errors$Plugins$OnErrorThrowAssertion"); // send something to the suppress handler, but we should get an AssertionError Errors.log().run(() -> { throw new RuntimeException("Didn't see this coming."); }); } finally { System.clearProperty("durian.plugins.com.diffplug.common.base.Errors.Plugins.Suppress"); DurianPlugins.resetForTesting(); Errors.resetForTesting(); } }
/** * Logs any exceptions. * <p> * By default, log() calls Throwable.printStackTrace(). To modify this behavior * in your application, call DurianPlugins.set(Errors.Plugins.Log.class, error -> myCustomLog(error)); * * @see DurianPlugins * @see Errors.Plugins.OnErrorThrowAssertion */ @SuppressFBWarnings(value = "LI_LAZY_INIT_STATIC", justification = "This race condition is fine, as explained in the comment below.") public static Handling log() { if (log == null) { // There is an acceptable race condition here - log might get set multiple times. // This would happen if multiple threads called log() at the same time // during initialization, and this is likely to actually happen in practice. // // Because DurianPlugins guarantees that its methods will have the exact same // return value for the duration of the library's runtime existence, the only // adverse symptom of this race condition is that there will temporarily be // multiple instances of Errors which are wrapping the same Consumer<Throwable>. // // It is important for this method to be fast, so it's better to accept // that suppress() might return different Errors instances which are wrapping // the same actual Consumer<Throwable>, rather than to incur the cost of some // type of synchronization. log = createHandling(DurianPlugins.get(Plugins.Log.class, Plugins::defaultLog)); } return log; }