Model Spec Builder

Declarative model specification for tests.

This document is concerned with internal implementation notes, not the API. For actual usage, see JavaDoc in Element.java, examples in ModelSpecBuilderTest.java or in any existing Expander test classes.

Basic structure

Current internal implementation makes heavy use of reflection; this can be now replaced with expanded code, but it was necessary to make it work first.

Basic signature:

public static ElementSpec <metaElement>(String spec, Spec... children);

Example usage:

ElementSpec spec =
  dataElement("City",
      field("name : String",
          fieldOption("isNameField")),
      field("country : Country[Ln01]"),
      dataOption("displayName : name_country"),
      dataOption("compositesEnabled"));

Note that the spec itself is a declarative description of the model, not the model itself.

AnyScopeBuilder

To build the spec, it must pass through a series of net.democritus.testing.internal.ScopeBuilder<SCOPE extends CompositeProjection> scope builders.

If no explicit ScopeBuilders is invoked, AnyScopeBuilder is used to process children.

dataElement("City",
  field("name"));

For any non-root (see later) element (in the example above field):

  1. create a new instance of the child class (FieldComposite),
  2. set its name to whatever was provided in spec ("name")
  3. process all its children in the same manner
  4. append the new instance to the parent using CompositeSynchronizedLinker

CompositeSynchronizedLinker reflectively adds a child to its parent; it tries any of the following (in this order):

  1. explicit CompositeSynchronizedLinker.appendTo(parent, child) method
  2. parent.set<child>(child) (applicable for Ln01 and Ln02)
    • e.g. field.setValueField(valueField)
  3. (for Ln05, Ln06)
    • 3.1. parent.get<childList>() + parent.set<childList>(childList)
      • e.g. dataElement.setFields(unmodifiableList(dataElement.getFields() + field))
    • 3.2. child.set<parent>(parent)
      • e.g. field.setDataElement(dataElement)

Root ScopeBuilders

Every element that can be potentially used as a root (namely applicationInstance; but for tests also application, and component) must implement ScopeBuilder<SCOPE extends *Composite> and override buildRoot(ElementSpec) : SCOPE method.

buildRoot() defines what the root object should look like.

Element Interception

To intercept a spec build, add a public void add<metaElement>(ElementSpec elementSpec) method to the ScopeBuilder of the metaElement's parent.

The method is additionally responsible for:

  • adding itself to the parent
  • invoking a child ScopeBuilder, if any

Note. Those two steps should eventually be automated in the base builder.

E.g. to intercept the field construction in

dataElement("City",
  finder("findByNameEq"))

add the following method

public class DataElementScopeBuilder extends ScopeBuilder<DataElementComposite> {

  public void addFinder(ElementSpec elementSpec) {
    // ...

    // CompositeSynchronizedLinker.dynamicallyAppend(...)
    dynamicallyAppend(root, finder);

    // if there's a custom ScopeBuilder
    new FinderScopeBuilder(finder, elementSpec).build();
    // or use the default one
    new AnyScopeBuilder(finder, elementSpec).build();
  }
}

Note that invoking a child builder is probably necessary if the element cannot have any children, e.g. dataOption(), but maybe it should remain consistent.

Setter Interception

The same interception approach applies for setters.

set("<attributeName>", <value>) : AttributeSpec
  1. try currentScopeBuilder.set<attributeName>(<value>)
  2. try currentScopeBuilder.root.set<attributeName>(<value>)

Example:

component(
  set("packageName", "net.democritus.elements"),
  dataElement("BusinessLogicSettings",
    set("packageName", "net.democritus.settings")))

ComponentSpecBuilder implements setPackageName(String packageName), so the first set() spec is intercepted by the builder even though ComponentComposite doesn't have any such method.

The second set() spec is not intercepted, so DataElementComposite::setPackageName() is used directly.

DataRef Interception

DataRefs currently cannot be intercepted and are called directly on the composite classes.

dataRef("attributeName", "(functional) dataRef as a string")

Example:

component("testComp",
  dataElement("Country"),
    taskElement("PopulationCounter",
      dataRef("targetElement", "testComp_Country")));

Will reflectively invoke:

populationCounterTaskElement.setTargetElement(countryDataElement);

results matching ""

    No results matching ""