code
docs
tests
Zest™ has a distinct bootstrap phase, also known as the Assembly of an application, where the applications structure is defined programmatically. Once all the layers, modules and all the composite types in each module have been defined the model is instantiated into an application. This enables the entire structure system in Zest, where types "belongs" to a module and visibility rules define default behaviors, enforcement of architectural integrity and much more.
The assembly is preceeded by the creation of the Zest Runtime. The assembly can be declared fully by defining
all modules and layers, and how the layers are sitting on top of each other, OR one can utilize one of the two
convenience assemblies, one for a pancake pattern, where all layers are top on each other, or one with a single module
in a single layer, useful for small applications, spikes and tests. The bootstrap
system has several ways to acheive
this, and they are listed below in Layered Application Assembler.
During assembly, the application (JVM level) architecture and the application model is defined. You define which layers exist and how they relate to each other. For each layer, you define which modules it contains. And for each module, you define which composites are in it, and what are the visibility rules for each of these composites.
You can also;
@Override public void assemble( ModuleAssembly module ) throws AssemblyException { module.objects( MyObject.class ).visibleIn( Visibility.layer ); }
@Override public void assemble( ModuleAssembly module ) throws AssemblyException { module.transients( MyTransient.class ).visibleIn( Visibility.layer ); }
@Override public void assemble( ModuleAssembly module ) throws AssemblyException { module.values( MyValue.class ).visibleIn( Visibility.layer ); }
@Override public void assemble( ModuleAssembly module ) throws AssemblyException { module.entities( MyEntity.class ).visibleIn( Visibility.layer ); }
@Override public void assemble( ModuleAssembly module ) throws AssemblyException { module.services( MyService.class ).visibleIn( Visibility.layer ); }
@Override public void assemble( ModuleAssembly module ) throws AssemblyException { module.services( MyService.class ).taggedWith( "foo", "bar" ); }
@Override public void assemble( ModuleAssembly module ) throws AssemblyException { module.importedServices( MyService.class ). importedBy( InstanceImporter.class ). setMetaInfo( new MyService() ); // OR module.objects( MyService.class ); module.importedServices( MyService.class ). importedBy( NewObjectImporter.class ); }
@Override public void assemble( ModuleAssembly module ) throws AssemblyException { module.values( MyValue.class ); MyValue myValueDefaults = module.forMixin( MyValue.class ).declareDefaults(); myValueDefaults.foo().set( "bar" ); module.entities( MyEntity.class ); MyEntity myEntityDefaults = module.forMixin( MyEntity.class ).declareDefaults(); myEntityDefaults.cathedral().set( "bazar" ); }
Many libraries and extensions provides a cookie-cutter Assembler, to simplify the set up of such component. Often these are suitable, but sometimes they won’t fit the application in hand, in which case the source code at least provides information of what is needed for the component to be used.
Assemblers are typically just instantiated and then call the assemble() method with the ModuleAssembly instance, such as;
@Override public void assemble( ModuleAssembly module ) throws AssemblyException { RestServerAssembler assembler = new RestServerAssembler(); assembler.assemble( module ); }
Defining an Entity Store is in principle as simple as defining a ServiceComposite implementing the EntityStore interface. The problem is that most Entity Stores require Service Configuration, and configuration requires an Entity Store. This chicken-and-egg problem is resolved by having an entity store available that does not require any Service Configuration. Many Assemblers for entity store implementations uses the MemoryEntityStore, and effectively leaves the configuration in the properties file where Service Configuration bootstraps from. It is possible to chain this, so that for instance the Neo4J Entity Store uses the Preferences Entity Store for its configuration, and the Preferences Entity Store uses the Memory Entity Store (i.e. the properties file).
The point is that the entity store used for the configuration of the primary entity store used in the application is that it must not be visible to the application itself. Sometimes it is easier to put a Memory Entity Store in the same module, with Visibility set to module. Sometimes it makes sense to have an additional Configuration layer below the infrastructure layer, which has this setup.
As mentioned above, most entity stores defines a reasonable default Assembler, possibly with some constructor arguments or methods to define certain aspects. An example is the popular JdbmEntityStore, which Assembler can be used like;
@Override public void assemble( ModuleAssembly module ) throws AssemblyException { new JdbmEntityStoreAssembler().assemble( module ); }
In 2.1, a new way to instantiate Zest™ applications was introduced. It starts with subclassing the
LayeredApplicationAssembler
, and implementing the assembleLayers()
method.
In the assembleLayers()
method, one is epected to either call the createLayer()
method in the super class
with the Class of the LayerAssembler,
LayerAssembly domainLayer = createLayer( DomainLayer.class );
OR manually instantiate and call the LayerAssembler.
LayerAssembly infraLayer = new InfrastructureLayer( configModule ).assemble( assembly.layer( InfrastructureLayer.NAME ));
This is to make the normal case as simple as possible, yet allow the special needs that occssionally surfaces.
Each LayerAssembler implementation may optionally extend the LayeredLayerAssembler
, to get access to the
createModule()
method, which again simplifies the creation of modules in the assemble()
method.
createModule( layer, InvoicingModule.class );
ModuleAssembler
implementations typically use Assembler
classes to put together, or call the entities()
,
values()
methods described elsewhere on this page. There is no superclass to use.
ModuleAssembler
implementations should have a name ending with "Module" and the naming will insert a human-readable
space within the module name, e.g. InvoicingModule
will be named "Invoicing Module".
For example code, see the tutorial Assemble an Application.
Every Zest™ runtime instance consist of One Application, with one or more Layers and one or more Modules in each Layer. So the minimal application is still one layer with one module. This is not recommended other than for testing purposes and really trivial applications.
Let’s take a closer look at how it is put together.
SingletonAssembler assembler = new SingletonAssembler() { @Override public void assemble( ModuleAssembly module ) throws AssemblyException { module.services( MyService.class ).identifiedBy( "Foo" ); module.services( MyService.class ).identifiedBy( "Bar" ); module.objects( Stuff.class ); } }; Module module = assembler.module(); Stuff stuff = module.newObject( Stuff.class );
Once the SingletonAssembler constructor returns, the Zest™ application is up and running.
The SingletonAssembler also makes common system resources available from the bootstrap code, such as Module, UnitOfWorkFactory and others. This is possible since there is only one Module.
Some applications has no need for runtime determination of the exact application structure, and no need for
advanced alterations to a staright-forward layered application structure. By using the ApplicationBuilder
it is possible to define the application structure from a JSON document, AND call the provided main()
class,
taking the JSON document as input on System.in
.
The format of the JSON document, directly reflects the application structure, such as
{ "name": "Build from JSON test.", "layers": [ { "name": "service", "uses": [ "domain", "config"] }, { "name": "donfig" }, { "name": "domain", "modules" : [ { "name" : "Invoicing", "assemblers" : [ "org.hedhman.niclas.bootstrap.InvoicingAssembler" ] } ] } ] }
At the moment, the JSON format only support Assembler
classes to do the work.
Another way to use the ApplicationBuilder
is to subclass it, optionally use the configureFromJSON()
method,
and then programmatically enhance the structure before calling newApplication()
.
There is one case that stands out as a common case, and forms a reasonable middle-ground. It is where each layer sits exactly on top of each other layer, like pancakes. Each layer will only use the layer directly below and only that layer. For this case we have a convenience setup. You create an Assembler[][][], where the outer-most array is each layer, the middle array is the modules in each layer, and the last array is a set of assemblers needed to put the things togather.
Let’s look at an example;
public static void main( String[] args ) throws Exception { zest = new Energy4Java(); Assembler[][][] assemblers = new Assembler[][][]{ { // View Layer { // Login Module new LoginAssembler() // : }, { // Main Workbench Module new MenuAssembler(), new PerspectivesAssembler(), new ViewsAssembler() // : }, { // Printing Module new ReportingAssembler(), new PdfAssembler() // : } }, { // Application Layer { // Accounting Module new BookkeepingAssembler(), new CashFlowAssembler(), new BalanceSheetAssembler() // : }, { // Inventory Module new PricingAssembler(), new ProductAssembler() // : } }, { // Domain Layer // : }, { // Infrastructure Layer // : } }; ApplicationDescriptor model = newApplication( assemblers ); Application runtime = model.newInstance( zest.spi() ); runtime.activate(); } private static ApplicationDescriptor newApplication( final Assembler[][][] assemblers ) throws AssemblyException { return zest.newApplicationModel( new ApplicationAssembler() { @Override public ApplicationAssembly assemble( ApplicationAssemblyFactory appFactory ) throws AssemblyException { return appFactory.newApplicationAssembly( assemblers ); } } ); }
Full Assembly means that you have the opportunity to create any layer/module hierarchy that are within the rules of the Zest™ runtime. It requires more support in your code to be useful, and the example below is by no means a recommended way to organize large application assemblies.
In principle, you first start the Zest™ runtime, call newApplication with an ApplicationAssembler instance and call activate() on the returned application. The ApplicationAssembler instance will be called with an ApplicationAssemblyFactory, which is used to create an ApplicationAssembly describing the application structure.
private static Energy4Java zest; private static Application application; public static void main( String[] args ) throws Exception { // Create a Zest Runtime zest = new Energy4Java(); application = zest.newApplication( new ApplicationAssembler() { @Override public ApplicationAssembly assemble( ApplicationAssemblyFactory appFactory ) throws AssemblyException { ApplicationAssembly assembly = appFactory.newApplicationAssembly(); buildAssembly( assembly ); return assembly; } } ); // activate the application application.activate(); } static void buildAssembly( ApplicationAssembly app ) throws AssemblyException { LayerAssembly webLayer = createWebLayer( app ); LayerAssembly domainLayer = createDomainLayer( app ); LayerAssembly persistenceLayer = createInfrastructureLayer( app ); LayerAssembly authLayer = createAuth2Layer( app ); LayerAssembly messagingLayer = createMessagingLayer( app ); webLayer.uses( domainLayer ); domainLayer.uses( authLayer ); domainLayer.uses( persistenceLayer ); domainLayer.uses( messagingLayer ); } static LayerAssembly createWebLayer( ApplicationAssembly app ) throws AssemblyException { LayerAssembly layer = app.layer( "web-layer" ); createCustomerWebModule( layer ); return layer; } static LayerAssembly createDomainLayer( ApplicationAssembly app ) throws AssemblyException { LayerAssembly layer = app.layer( "domain-layer" ); createCustomerDomainModule( layer ); // : // : return layer; } static LayerAssembly createInfrastructureLayer( ApplicationAssembly app ) throws AssemblyException { LayerAssembly layer = app.layer( "infrastructure-layer" ); createPersistenceModule( layer ); return layer; } static LayerAssembly createMessagingLayer( ApplicationAssembly app ) throws AssemblyException { LayerAssembly layer = app.layer( "messaging-layer" ); createWebServiceModule( layer ); createMessagingPersistenceModule( layer ); return layer; } static LayerAssembly createAuth2Layer( ApplicationAssembly application ) throws AssemblyException { LayerAssembly layer = application.layer( "auth2-layer" ); createAuthModule( layer ); return layer; } static void createCustomerWebModule( LayerAssembly layer ) throws AssemblyException { ModuleAssembly assembly = layer.module( "customer-web-module" ); assembly.transients( CustomerViewComposite.class, CustomerEditComposite.class, CustomerListViewComposite.class, CustomerSearchComposite.class ); } static void createCustomerDomainModule( LayerAssembly layer ) throws AssemblyException { ModuleAssembly assembly = layer.module( "customer-domain-module" ); assembly.entities( CustomerEntity.class, CountryEntity.class ); assembly.values( AddressValue.class ); } static void createAuthModule( LayerAssembly layer ) throws AssemblyException { ModuleAssembly assembly = layer.module( "auth-module" ); new LdapAuthenticationAssembler().assemble( assembly ); new ThrinkAuthorizationAssembler().assemble( assembly ); new UserTrackingAuditAssembler().assemble( assembly ); } static void createPersistenceModule( LayerAssembly layer ) throws AssemblyException { ModuleAssembly assembly = layer.module( "persistence-module" ); // Someone has created an assembler for the Neo EntityStore new NeoAssembler( "./neostore" ).assemble( assembly ); }