Decomposition

Guide for writing re-usable bundles

In dropwizard we have only one decomposition element: ConfiguredBundle.

Note

In pure dropwizard, bundles also used inside single application to separate configuration blocks (simplify logic). In guicey, this is not required as classpath scan could be used for extensions registration (configuration amount reduced).

This chapter describe only re-usable logc decomposition.

In guicey we have: guicey bundle (GuiceyBundle), guice module (Module) and dropwizard bundle (ConfiguredBundle). This could confuse.

All these modules are supposed to be used together (obviously). In some cases, guicey explicitly provides wrapping modules (e.g. jdbi - wrapper for dropwizard module), but such wrappers usually provide additional abilities (impossible with pure module) and not driven by inability to use a raw module.

Guicey bundle

As it was described, guicey have to introduce its own bundle because guicey provides additional configuration features (even in the simplest case you should be able to configure guice modules).

Prefer GuiceyBundle over dropwizard Bundle for developing re-usable modules. Of course, if module is very generic (does not depend on guice) you can do pure dropwizard module (to publish it for wider audience), but almost always bundles rely on guicey features.

Benefits:

Dropwizard bundle

It is important to note that there is a difference between registration of dropwizard bundle directly (in dropwizard Bootstrap) and through guicey api: bundle registered through guicey api could be disabled, de-duplicated, tracked for transitive bundles.

Also, bundles registered through guicey api appear in configuration report.

There is a difference between dropwizard and guicey bundles in transitive bundles initialization:

Dropwizard bundle immediately initialize transitive bundle:

public class MyBundle implements ConfiguredBundle {
    @Override
    public void initialize(Bootstrap<?> bootstrap) {
            bootstrap.addBundle(new MyOtherBundle());  

            // line executed after MyOtherBundle init
    }
}

Guicey bundle register transitive bundle after current bundle, but dropwizard bundle immediately.

public class MyBundle implements GuiceyBundle {
    @Override
    public void initialize(GuiceyBootstrap bootstrap) {
            bootstrap
                .dropwizardBundles(new DwBundle())
                .bundles(new MyOtherBundle());

            // line executed before MyOtherBundle init, but after DwBundle init
    }
}

Guicey bundles behaviour explained by de-duplciation logic: registered root bundles must be initialized in priority. It avoids situation like:

GuiceBundle.builder()
    .bundles(new Bundle1(), new Bundle2(12))

If Bundle2 is unique and Bundle1 transitively installs new Bundle2(1) (e.g. with different config), then this transitive bundle would be ignored (because root bundles init will appear first). If it were working like dropwizard bundles, then directly registered Bundle2 would be ignored and remaining instance would have different configuration (confusion point).

Normally, this behaviour should not be an issue as you shouldn't rely on bundles initialization order. But this may be important with shared state.

Bundle vs Module

When extracting functionality into re-usable module always start with a bundle. Guice module, most likely, will be required too.

Logic should be separated as:

  • Guice module is responsible for guice bindings and should not be aware of dropwizard.
  • Bundle works with dropwizard: extract required configuration for creating module and do other registrations.

That's an ideal case. But, for example, if you need to apply some bindings based on configuration only then you can do it with pure guice module, like:

public class ModuleConfig {  
    @JsonProperty
    private String something; 
}

Module knows that target application (where this re-usable module would be used) will declare this configuration inside its main configuration:

public class AppConfig extends Configuration {
    @JsonProperty    
    private ModuleConfig module;
}

Make module aware of dropwizard staff:

public class ModuleImpl<C extends Configuration> extends DropwizardAwareModule<C> {

     @Override
     protected void configure() {
        // obtain sub-configuration object 
        ModuleConfig config = Preconditions.checkNotNull(configuration(ModuleConfig.class),
                "ModuleConfig is not found within application config. Please declare it.");

        // use it for binding    
        bind(SomeService.class).annotatedWith(Names.named(config.getSomething())).to(SomeServiceImpl.class);                            
     }
}

Warning

This is not a recommended way! It was shown just to demonstrate that guice module could be used without bundle. It's better to use declaration bundle instead:

public class ModuleImpl extends AbstractBundle {

     private ModuleConfig config;

     public ModuleImpl(ModuleConfig config) {
        this.config = config;
     }        

     @Override
     protected void configure() {                    
        bind(SomeService.class).annotatedWith(Names.named(config.getSomething())).to(SomeServiceImpl.class);                            
     }
}

public class ModuleBundle extends GuiceyBundle {
    @Override   
    public void run(GuiceyEnvironment environment) throws Exception {
         ModuleConfig config = Preconditions.checkNotNull(environment.configuration(ModuleConfig.class),
                    "ModuleConfig is not found within application config. Please declare it.");

          environment.modules(new ModuleImpl(config));          
    }
}

Bundle tips

These tips show various techniques for developing bundles.
Mostly, these tips are based on developing guicey extensions. (look extensions sources as examples).

Uniqueness

For everything that is registered "by instance" applied de-duplication mechanism.

You can use it to grant only one instance of bundle by extends UniqueGuiceyBundle (or more sophisticated logic by manual equals and hash code implementation, for example, to de-duplicate only instance with the same constructor arguments).

It may be mostly important for guice modules as guice will not start with duplicate bundings (MyModule extends UniqueModule).

Auto-loaded bundle

Auto loading based on guicey bundles lookup.

Be aware that user may switch off bundles lookup (with .disableBundleLookup()) or apply custom lookup).

Auto load override

If your bundle provides configuration, but you still want to load it automatically with the default configuration, then you can use bundle uniquness:

public class AutoLoadableBundle extends UniqueGuiceyBundle { ... }

Now only one bundle instance is allowed and, if user register bundle manually, bundle from lookup will simply be ignored. Lifecycle annotations module use this technique.

Optional extensions

All extensions must be registered under initialization phase, when configuration is not yet available and so it is not possible to implement optional extension registration.

To workaround this, you can conditionally disable extensions:

public class MyFeatureBundle implements GuiceyBundle {

    @Override
    public void initialize(GuiceyBootstrap bootstrap) {   
        // always register extension
        bootstrap.extensions(OptionalExtension.class);     
    }   

    @Override    
    public void run(GuiceyEnvironment environment) throws Exception {
        // disable extension based on configuration value
        if (!environment.configuration().getSomeValue()) {
            environment.disableExtension(OptionalExtension.class);
        }
    }
}

Replace features

As bundle has almost complete access to configuration, it can use disables to substitute application functions.

For example, it is known that application use ServiceX (from some core module in organization), but this bundle requires modified service. It can disable core module, installing feature and register customized module:

public class MyFeatureBundle implements GuiceyBundle {

    @Override
    public void initialize(GuiceyBootstrap bootstrap) {           
        bootstrap
            .disableModules(CoreModule.class)
            .modules(new CustomizedCoreModule());     
    }      
}      

Note

It's not the best example: of course it's simpler to use binding override to override single service. But it's just to demonstrate the idea (it could be repalced extension or fixed installer).

Bundles can't disable other bundles (because target bundle could be already processed at this point).

Bundle options

Bundle could use guicey options mechanism to access guicey option values:

public class MyBundle implements GuiceyBundle {
    @Override
    public void initialize(GuiceyBootstrap bootstrap) {
        if (bootstrap.option(GuiceyOptions.UseHkBridge)) {
            // show warning that bridge required
        } 
    }
}

Or it could be some custom options usage.

Note

Option values are set only in main GuiceBundle and so all bundles see the same option values (no one can change values after).

Configuration access

Bundle could access not only direct dropwizard Configuration, but also individual values
thanks to yaml values introspection.

Unique sub config

When creating re-usable bundle it is often required to access yaml configuration data. Usually this is solved by some "configuration lookups" like in dropwizard-views

Guicey allows you to obtain sub-configuration object directly:

public class XFeatureBundle implements GuiceyBundle {
    @Override
    public void run(GuiceyEnvironment environment) throws Exception {
        XFeatureConfig environment = bootstrap.configuration(XFeatureConfig.class);
        ...
    }
}

Note that this bundle doesn't known exact type of user configuration, it just assumes that XFeatureConfig is declared somewhere in configuration (on any level) just once. For example:

public class MyConfig extends Configuration {

    @JsonProperty
    private XFeatureConfig xfeature;

    ...
}

Important

Your sub configuration object must appear only once within user configuration.

Object uniqueness checked by exact type match, so if configuration also contains some extending class (XFeatureConfigExt extends XFeatureConfig) it will be different unique config.

Access by path

When you are not sure that configuration is unique, you can rely on exact path definition (of required sub configuration):

public class XFeatureBundle implements GuiceyBundle {
    private String path;

    public XFeatureBundle(String path) {
        this.path = path;
    } 

    @Override
    public void run(GuiceyEnvironment environment) throws Exception {
        XFeatureConfig conf = environment.configuration(path);
        ...
    }
}

Path is declared by bundle user, who knows required configuration location:

GuiceBundle.builder()
    .bundles(new XFeatureBundle("sub.feature"))
    ...
    .build()

Where

public class MyConfig extends Configuration {

    @JsonProperty
    private SubConfig sub = { // pseudo code to combine class declarations
         @JsonProperty
         private XFeatureConfig feature;   
    }

    ...
}

Multiple configs

In case, when multiple config objects could be declared in user configuration, you can access all of them:

public class XFeatureBundle implements GuiceyBundle {
    @Override
    public void run(GuiceyEnvironment environment) throws Exception {
        List<XFeatureConfig> confs = environment.configurations(XFeatureConfig.class);
        ...
    }
}

For configuration

public class MyConfig extends Configuration {

    @JsonProperty
    private XFeatureConfig xfeature;
    @JsonProperty
    private XFeatureConfig xfeature2;

    ...
}

It wil return both objects: [xfeature, xfeature2]

Important

In contrast to unique configurations, this method returns all subclasses too. So if there are XFeatureConfigExt extends XFeatureConfig declared somewhere it will also be returned.

Custom configuration analysis

In all other cases (with more complex requirements) you can use ConfigurationTree object which represents introspected configuration paths.

public class XFeatureBundle implements GuiceyBundle {
    @Override
    public void run(GuiceyEnvironment environment) throws Exception {
         // get all properties of custom configuration (ignoring properties from base classes)
        List<ConfigPath> paths = environment.configurationTree()
                .findAllRootPathsFrom(MyConfig.class);

        // search for not null values of marked (annotated) classes            
        List markedTypes = paths.stream()
            .filter(it -> it.getValue() != null 
                    && it.getType().getValueType().hasAnnotation(MyMarker.class))
            .map(it -> it.getValue())
            .collect(Collectors.toList());
        ...
    }
}

In this example, bundle search for properties declared directly in MyConfig configuration class with not null value and annotated (classes annotated, not properties!) with custom marker (@MyMarker).

See introspected configuration structure description.

Shared state

Guicey maintains special shared state object useful for storing application-wide data.

Warning

Yes, any shared state is a "hack". Normally, you should avoid using it. Guicey provides this ability to unify all such current and future hacks: so if you need to communicate between bundles - you don't need to reinvent the wheel and don't have additional problems in tests (due to leaking states).

For example, it is used by spa bundle to share list of registered application names between all spa bundle instances and so be able to prevent duplicate name registration.

Server pages bundle use shared state to maintain global configuration and allow application bundles communication with global views bundle.

Equal communication scenario

Case when multiple (equal) bundles need to communicate. In this case first initialized bundle init shared state and others simply use it.

public class EqualBundle implements GuiceyBundle {
    @Override
    public void initialize(GuiceyBootstrap bootstrap) {
        // either obtain already shared object or share new object                                                             
        SomeState state = bootstrap.sharedState(EqualBundle, () -> new SomeState());
        ...
    }
}

Parent-child scenario

Case when there is one global bundle, which must initialize some global state and child bundles, which use or appends to this global state.

public class GlobalBundle implements GuiceyBundle {
    @Override
    public void initialize(GuiceyBootstrap bootstrap) {
        // share global state object
        bootstrap.shareState(GlobalBundle, new GlobalState());
    }        
}    

public class ChildBundle implements GuiceyBundle {
    @Override
    public void initialize(GuiceyBootstrap bootstrap) {
        // access shared object or fail when not found
        GlobalState state = environment.sharedStateOrFail(GlobalBundle, 
                "Failed to obtain global state - check if global bundle registered");
    }        
}

Before/after run logic

If multiple bundles must be synchronized on run phase, use guicey events.

To run code after all guicey bundles initialization, but before run:

@Override
public void initialize(final GuiceyBootstrap bootstrap) {
    bootstrap.listen(new GuiceyLifecycleAdapter() {
        @Override
        protected void beforeRun(final BeforeRunEvent event)  {
            // do something before bundles run
            // NOTE that environment and configuration already available!
        }
    });
}

To run code after all guicey bundles run methods (delayed init):

@Override
public void run(final GuiceyEnvironment environment) {
    environment.listen(new GuiceyLifecycleAdapter() {
        @Override
        protected void bundlesStarted(final BundlesStartedEvent event) {
            // still sdropwizard run phase (anything could be configured)
            // but all guicey bundles aready executed 
        }
    });
}

Note

This will work only for guicey bundles! Registered dropwizard bundles may execute before or after these events: events broadcast from main dropwizard GuiceBundle run method, so other dropwizard bundles, registered after guice bundle will run after it. It is assumed that guicey bundles used for most configurations (especially in complex cases when bundles synchronization is required)).