1. Developing applications with edoras one

This document describes best practices for developing a customized application based on edoras one. It is mainly intended for software developers, although some topics address system installation and configuration and may therefore also be of interest to system operators.

Various topics are presented, grouped by general theme, with each topic describing a specific development task. The topics are generally independent of each other and can be read on their own without having to read the entire document. A working knowledge of the concepts underlying edoras one is required, and some topics will also require knowledge of edoras gear or some additional external technologies.

2. Setting up a development environment

Developing applications with edoras one is fairly straightforward if you are familiar with standard Java development workflows:

  • create a new project using a build tool (e.g. Maven or Gradle)

  • include the edoras one artifacts in your project as normal dependencies

  • provide the basic configuration needed to start edoras one

  • extend edoras one by adding your own configurations and / or classes as required

This section describes setting up your development environment and including edoras one into your project. The following sections describe the edoras one extension points and how they can be used to customize edoras one and integrate it into your IT infrastructure.

The minimum Java version to develop with edoras one is Java 8.

2.1. Configuring the build system

2.1.1. Basic project build information

The example project provides build configurations for both the Maven and Gradle build systems. You can use whichever build system you prefer. For the Maven build Maven 3.0.0 or greater is required. The Gradle build is based on the Gradle wrapper and downloads the appropriate version automatically.

You can download the build tools here:

Basic Maven configuration

The project information and compiler version (Java 7 is required) should be set in the Maven pom.xml:

Project information in pom.xml
  <project xmlns="http://maven.apache.org/POM/4.0.0"
                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                               http://maven.apache.org/xsd/maven-4.0.0.xsd">
          <modelVersion>4.0.0</modelVersion>

          <groupId>com.edorasware.one</groupId>
          <artifactId>edoras-one-bootstrap</artifactId>
          <version>1.0.0</version>
          <packaging>war</packaging>
Compiler configuration in pom.xml
  <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.1</version>
          <configuration>
                  <source>1.8</source>
                  <target>1.8</target>
          </configuration>
  </plugin>

You should also configure the resource encoding to avoid warnings during the build:

Resource configuration in pom.xml
  <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-resources-plugin</artifactId>
          <version>2.6</version>
          <configuration>
                  <encoding>UTF-8</encoding>
          </configuration>
  </plugin>
Basic Gradle configuration

First you need to apply the war plugin and define the project information:

Project information in build.gradle
  apply plugin: 'war'

  allprojects {
    group = 'com.edorasware.one'
    version = '1.0.0'

    sourceCompatibility = 1.8
    targetCompatibility = 1.8
  }

2.1.2. Artifact repository configuration

To build the project you will need access to a Maven repository containing the edoras one artifacts so that the edoras one dependencies can be resolved. The artifacts can either be uploaded to a repository manually, or if you have access to the edorasware public repository then the connection details may be configured into your local repository and artifacts will be downloaded from edorasware automatically when they are needed.

After adding the edoras one artifacts to your repository you will also need to provide a suitable repository configuration and access credentials.

Maven repository configuration

The Maven repository configuration will typically be located in the pom.xml file. The following snippet shows the configuration for the edoras repository. The configuration for your local repository will look similar.

Repository configuration in pom.xml for Maven builds
  <repositories>
          <repository>
                  <id>repo.edorasware.com</id>
                  <url>https://repo.edorasware.com/edoras-repo</url>
                  <snapshots>
                          <enabled>true</enabled>
                          <updatePolicy>always</updatePolicy>
                  </snapshots>
          </repository>
  </repositories>

  <pluginRepositories>
          <pluginRepository>
                  <id>repo.edorasware.com</id>
                  <url>https://repo.edorasware.com/edoras-repo</url>
          </pluginRepository>
  </pluginRepositories>

Add the repository credentials to the servers section in your local Maven configuration file. By default you can find the Maven configuration file at <USER_HOME>/.m2/settings.xml.

Add this block to the settings file
  <servers>
      <server>
          <id>repo.edorasware.com</id>
          <username>customer-username</username>
          <password>customer-password</password>
      </server>
  </servers>

Additional details of how to configure a Maven repository can be found on the Maven project page.

Gradle repository configuration

The Gradle repository configuration will typically be located in the build.gradle file. The following snippet shows the configuration for the edoras repository. The configuration for your local repository will look similar.

Repository configuration in build.gradle for Gradle builds
  repositories {
    mavenLocal()
    maven {
      credentials {
        username DOWNLOAD_REPO_USERNAME
        password DOWNLOAD_REPO_PASSWORD
      }
      url "https://repo.edorasware.com/edoras-repo"
    }
    mavenCentral()
  }

Add the credentials that you received from edorasware to your local gradle.properties file. By default you can find the Gradle configuration file at <USER_HOME>/.gradle/gradle.properties.

Add this block to the gradle.properties file
  DOWNLOAD_REPO_USERNAME=customer-username
  DOWNLOAD_REPO_PASSWORD=customer-password

2.1.3. edoras one dependency configuration

The edoras one artifacts can now be added to the project as a dependency.

Maven dependency configuration

By adding a property to the <properties> tag of the Maven pom.xml file the edoras one version can be set in a single place:

edoras one version property in the Maven pom.xml
  <properties>
          <com.edorasware.one.version>@projectVersion@</com.edorasware.one.version>
  </properties>

This property can then be used in the dependency configuration to import the required dependencies:

edoras one dependencies in the Maven pom.xml
  <!-- Compile dependencies -->
  <dependency>
          <groupId>com.edorasware.one</groupId>
          <artifactId>edoras-frontend</artifactId>
          <version>${com.edorasware.one.version}</version>
  </dependency>

  <dependency>
          <groupId>com.edorasware.one</groupId>
          <artifactId>edoras-one-server-rest</artifactId>
          <version>${com.edorasware.one.version}</version>
          <exclusions>
                  <!-- TODO fix for https://issues.apache.org/jira/browse/BATIK-1038 -->
                  <exclusion>
                          <groupId>org.apache.xmlgraphics</groupId>
                          <artifactId>batik-extensions</artifactId>
                  </exclusion>
          </exclusions>
  </dependency>
  <dependency>
          <groupId>com.edorasware.license</groupId>
          <artifactId>edoras-license-one</artifactId>
          <classifier>development</classifier>
          <version>1.0.7</version>
  </dependency>
Gradle dependency configuration

By adding a property to the <properties> tag of the Gradle build.gradle file the edoras one version can be set in a single place:

edoras one version property in the Gradle build.gradle
  def edorasOneVersion = '@projectVersion@'

This property can then be used in the dependency configuration to import the required dependencies:

edoras one dependencies in the Gradle build.gradle
  compile "com.edorasware.one:edoras-frontend:$edorasOneVersion"
  compile("com.edorasware.one:edoras-one-server-rest:$edorasOneVersion")

  compile "com.edorasware.one:edoras-one-index:$edorasOneVersion"

  testCompile "com.edorasware.one:edoras-test:$edorasOneVersion"
  testRuntime "com.edorasware.license:edoras-license-one:1.0.7:development"

2.1.4. Database dependency configuration

In addition to the edoras one dependencies, at least one database dependency is also required, e.g.:

Database dependencies in pom.xml for Maven builds
  <dependency>
          <groupId>com.h2database</groupId>
          <artifactId>h2</artifactId>
          <version>1.4.194</version>
  </dependency>
Database dependencies in build.gradle for Gradle builds
  compile "com.h2database:h2:1.4.194"

2.1.5. Logging dependency configuration

edoras one uses the Simple Logging Facade for Java (SLF4J) for logging. SLF4J is a framework that allows the end user to plug in the desired logging framework at deployment time. You can learn more about slf4j on the {slf4j-page}.

Maven dependency configuration

By adding a property to the <properties> tag of the Maven pom.xml file the slf4j version can be set in a single place:

Logging version property in the Maven pom.xml
  <org.slf4j.version>1.7.25</org.slf4j.version>

This property can then be used in the dependency configuration to import the required dependencies. The following dependencies instruct SLF4J to send all logging output from components used in edoras one to slf4j:

Basic logging dependencies in pom.xml for Maven builds
  <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>jul-to-slf4j</artifactId>
          <version>${org.slf4j.version}</version>
  </dependency>

  <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>jcl-over-slf4j</artifactId>
          <version>${org.slf4j.version}</version>
  </dependency>

  <dependency>
          <artifactId>commons-logging</artifactId>
          <groupId>commons-logging</groupId>
          <version>1.2</version>
          <scope>provided</scope>  <!-- globally replace commons-logging with jcl-over-slf4j -->
  </dependency>

A suitable adapter can then be configured to send the slf4j output to a particular logging system. Foe example we can include the slf4j-to-log4j adapter:

Logging adapter in pom.xml for Maven builds
  <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-log4j12</artifactId>
          <version>${org.slf4j.version}</version>
  </dependency>
Gradle dependency configuration

By adding a property to the <properties> tag of the Gradle build.gradle file the slf4j version can be set in a single place:

Logging version property in the Gradle build.gradle
  def slf4jVersion = '1.7.25'

This property can then be used in the dependency configuration to import the required dependencies. The following dependencies instruct SLF4J to send all logging output from components used in edoras one to slf4j:

Basic logging dependencies in the Gradle build.gradle
  runtime "org.slf4j:jul-to-slf4j:$slf4jVersion"
  runtime "org.slf4j:jcl-over-slf4j:$slf4jVersion"

  // globally replace commons-logging with jcl-over-slf4j
  providedRuntime "commons-logging:commons-logging:1.2"

A suitable adapter can then be configured to send the slf4j output to a particular logging system. For example to use log4j, so we include the slf4j-to-log4j adapter:

Logging adapter in the Gradle build.gradle
  runtime "org.slf4j:slf4j-log4j12:$slf4jVersion"

2.2. License file

To start edoras one you will need a valid edoras one license file. The license location is defined by the system.license-location property.

2.3. Build and deploy the WAR file

The project WAR file can now be built using the Maven package target or the Gradle war task. The generated war can then be deployed in a suitable application server.

3. Configuring edoras one

edoras one is configured using Spring Java configurations. For details of the Spring project, please refer to the {spring-page}.

The basic configuration is provided by the edoras one artifacts and can be reused, although it can be customized in a number of ways:

3.1. Additional Spring configurations

To add your own Spring configuration to the application context, simply add your configuration class to the package com.edorasware.config.custom and annotate it with the standard Spring annotation @Configuration:

  @Configuration
  public class BootstrapConfiguration {

      // custom configuration

  }

This is simply a plain Spring configuration file and configurations can be added using the standard Spring functionality. In this case we also import additional indexing configurations (provided by edoras one) and configure an additional component scan.

Project-specific beans can be added directly to this configuration in the standard way:

  @Bean
  public StaticReferenceDataManager staticReferenceDataManager() {
      List<String> countries = new ArrayList<>();
      countries.add("Switzerland");
      countries.add("Swaziland");
      countries.add("Spain");

      return new StaticReferenceDataManager(countries);
  }

In addition to defining new project-specific Spring configurations and beans, the functionality of the edoras one product can also be extended. The extension points in the public Java API are marked in the Javadoc by the @ExtensionPoint annotation. For each extension point, the Javadoc should contain basic instructions on how the extension point can be used. A summary of each extension point together with specific use case information is provided in the section Customizing edoras one.

3.2. Property settings

Properties have default values provided by edoras one and these can be overwritten by adding property definitions to one of the following locations:

  • an application.properties (as a resource in the project root package)

  • a custom property file location set by the property system.custom-properties

  • the one.properties file in the edoras-one.home location

  • system properties

Properties defined later in this list have priority (so a system property setting has precedence over a property setting in the application.properties file).

3.3. Configuration customization

The default edoras one configurations can be customized directly for specific supported use cases using the ConfigurationConfigurer implementations in the project-specific configuration. Each part of the base configuration that supports customization provides a specific implementation of the ConfigurationConfigurer interface. For example the property configuration can be customized using the PropertyConfigurationConfigurer.

To customize a given part of the base configuration, simply declare a bean with the correct interface, and implement the configure() method. The builder parameter to this method provides an API that supports the available customizations. In this example we customize the property configuration to add an additional blacklisted property name:

  @Bean
  public PropertyConfigurationConfigurer propertyConfigurationConfigurer() {
      return new PropertyConfigurationConfigurer() {
          @Override
          public void configure(PropertyConfigurationBuilder builder) {
              builder.addBlacklistedPropertyNames("secret");
          }
      };
  }
These bean declarations can usually be expressed more concisely using Java 8 lambda expressions.

3.4. Overriding edoras one bean definitions

Generally, the officially supported mechanisms should be used to extend or modify the application for a particular use case. From time to time the supported mechanisms may be insufficient, however, and in this case it may be necessary to overwrite bean definitions from the base configuration directly. Doing so is not supported and may break without notice when upgrading to a newer edoras one version.

The default edoras one Spring bean configurations can be overridden if a new Spring bean is defined with the same ID as the base configuration (if two bean definitions have the same ID then the last definition wins). The bean ID in the case of Java configuration is the method name used to create the bean, so simply copy the bean creation method from the base configuration and replace it with the implementation that you require.

Note that application-specific configurations will be loaded last by default. If you need to control the order in which configurations are loaded then you can use the @Order annotation on the configuration class.

4. Configuring edoras vis

The edoras vis configuration is optional as the defaults should be sufficient for most project. If you still need to change the palette or adapt the edoras vis editor then please read on.

4.1. Palette configuration

Palette configuration should be added into the project’s Spring configuration as shown below.

  <bean id="paletteConfiguration" class="com.edorasware.bpm.modeler.config.PaletteConfiguration">
      <property name="paths">
          <list>
              <value>classpath:com/edorasware/vis/palette</value>
          </list>
      </property>
      <property name="baseProcessPalette" value="classpath:com/edorasware/vis/palette/base.process.palette.xml"/>
      <property name="baseFormPalette" value="classpath:com/edorasware/vis/palette/base.form.palette.xml"/>
      <property name="baseCasePalette" value="classpath:com/edorasware/vis/palette/base.case.palette.xml"/>
      <property name="defaultProcessPaletteName" value="default.process.palette.xml"/>
      <property name="defaultFormPaletteName" value="default.form.palette.xml"/>
      <property name="defaultCasePaletteName" value="default.case.palette.xml"/>
  </bean>
Table 1. Supported palette configuration bean properties
Name Description

paths

List of palette folder paths

baseProcessPalette

Fully qualified base process palette file name

baseFormPalette

Fully qualified base form palette file name

baseCasePalette

Fully qualified base case palette file name

defaultProcessPaletteName

Fully qualified default process palette file name

defaultFormPaletteName

Fully qualified default form palette file name

defaultCasePaletteName

Fully qualified default case palette file name

4.2. Editor configuration

Editor configuration should be added into the project’s Spring configuration as shown below.

  <bean id="editorConfiguration" class="com.edorasware.bpm.modeler.config.EditorConfiguration">
      <property name="disableSaveDialog" value="true"/>
      <property name="showSystemFormPalette" value="false"/>
      <property name="showSystemProcessPalette" value="false"/>
      <property name="showSystemCasePalette" value="false"/>
      <property name="saveNotificationUrl" value="../rest/modeler"/>
  </bean>
Table 2. Supported editor configuration bean properties
Name Description

disableSaveDialog

Flag to enable or disable showing the save dialog for each save operation

showSystemFormPalette

Flag to show/hide system form palette in the form designer

showSystemProcessPalette

Flag to show/hide system process palette in the process designer

showSystemCasePalette

Flag to show/hide system case palette in the case designer

saveNotificationUrl

URL to which the save notification will be posted

5. Work object API

The edoras one runtime environment uses a unified representation for all workflow items (e.g. cases, tasks, processes) that is accessed through the work object API. The work object API has a number of aspects:

Multi-tenant

A single edoras one server can support multiple tenants. A tenant is a separate, self-contained partition of the persistence layer where the work objects for a particular group of users are stored. A user is assigned to a particular tenant, and normally only work objects within that tenant will be accessible.

Persistence and transactions

Work objects are backed by a transactional JDBC persistence layer. All work object operations should be carried out within a valid JDBC transaction. A work object is a collection of persistent values that may have a limited range of types (see the Javadoc for the package com.edorasware.api.value). Some values are predefined and are common to all work objects, but a work object can also contain any number of additional type-specific or use-case specific values.

Hierarchical organisation

Work objects are organised in a tree hierarchy, where a work object may either be unattached or have a single parent work object. Work object operations can make use of this hierarchy information, e.g. to search for all open tasks within the scope of a given case or find the top-most parent process for a given work object.

Provider-based architecture

The provider architecture used by the work object framework allows edoras one to integrate with external systems. For example edoras one has providers that integrate with the Flowable process engine, mirroring process executions and user tasks to work objects in edoras one. Work objects can be manipulated and searched for using a single API regardless of the provider that is responsible for managing them.

Work object definitions

A work object can either be created from scratch on an ad-hoc basis by application code, or it can be created from a work object definition which acts as a template for the work object and its behaviour. Work object definitions are normally created from the models in an App when that App is deployed.

As an example, when a process work object is created with a reference to a particular process definition the relevant provider will be notified and can perform other actions in response to the work object creation, in this case starting a new process execution in the Flowable process engine.

Once a work object has been created, it keeps a reference to the definition that it was created from, so the behaviour of the work object will normally remain unchanged even if the App is redeployed to create an updated definition.

Lifecycle listeners

The work object framework also supports the use of listeners for work object lifecycle events (such as state changes or changes to the values stored in a work object). These listeners can either be used to simply respond to particular changes or may actively modify the changes that will be made to the work object before they are persisted.

5.1. Work object structure

A work object is a unit of data persistence, which has a collection of values, some of which are common to all work objects and some of which are used only by specific work object types or even specific instances. The common values generally have specific accessor methods in addition to being accessible through the generic value access interfaces.

Among the values stored on a work object are IDs used to identify the work object in a particular context, or to identify objects related to that work object instance. The IDs used are:

Work object ID

The work object ID is unique across all tenants and systems and identifies the work object uniquely.

Tenant ID

The tenant ID identifies the tenant containing the work object. For most application code, the current user context defines the tenant to be used, and only work objects from that tenant will be accessible. The tenant ID is mainly of interest to framework code that is operating across multiple tenants.

Global ID

The global ID is only unique within the scope of a particular tenant. The same global ID may be used for work objects that represent the 'same' entity in different tenants, the . An example is a model work object, which has the same global ID for the same model on all tenants and all installations.

External ID

The external ID can be used by the work object provider to link to an external system. It is normally not of interest to application code.

Provider ID

The optional provider ID links to the provider that is responsible for managing the work object.

Definition ID

The optional definition ID links to the specific definition from which the work object is derived.

5.2. Working with work objects

The main starting point for working with work objects is the service WorkObjectService.

5.2.1. Creating work objects

Using the service interface WorkObjectService you can either create a work object directly from a definition or create a work object with all of the values that you require in memory and then persist it.

To create a new work object in memory, use the WorkObject builder method. You can also build a new in-memory work object using another work object as a template. Once the work object has been created, it can be persisted using one of the create methods on the work object service:

  WorkObject toDoTasks = WorkObject.builder()
          .type(WorkObjectTypes.CASE_TYPE)
          .name("My ToDo task list")
          .build();

  String toDoListWorkObjectId = this.workObjectService.create(toDoTasks);

A work object can also be added as a child of another work object:

  WorkObject milkTask = WorkObject.builder()
          .type(WorkObjectTypes.TASK_TYPE)
          .name("Order the milk")
          .build();

  String taskId = this.workObjectService.create(milkTask, toDoListWorkObjectId);

To base a work object on a particular definition, simply add the definition ID to the in-memory work object before it is persisted. The provider will be notified and any necessary actions performed automatically. For example starting a new process from a process definition will start a new process execution in the Flowable engine:

  WorkObject process = WorkObject.builder()
          .type(WorkObjectTypes.PROCESS_TYPE)
          .definitionId(processDefinition.getId())
          .name("Test Process")
          .build();

  String processId = this.workObjectService.create(process);

5.2.2. Modifying work objects

Work objects can be modified using a work object update builder. To get an update builder for a particular work object the methods createUpdateBuilder or createReadableUpdateBuilder should be used. Using an update builder, multiple changes can be made to the same work object and then persisted in a single step using the apply() method:

  WorkObjectUpdateBuilder updateBuilder =
          this.workObjectService.createUpdateBuilder(taskId);

  WorkObject updatedWorkObject = updateBuilder
          .name("Reassigned task name")
          .ownerId(newOwnerId)
          .apply();

The readable update builder also allows the current work object values to be read from the builder itself, although this incurs additional overhead when the update builder is created.

5.2.3. Querying for work objects

The WorkObjectService interface allows work objects to be searched for using a Predicate or Predicate. Either a specific matching work object, a list of matching work objects, or the count of matching work objects can be returned.

The easiest way to create a predicate for a particular work object value is to use the relevant constant in the WorkObjectValues class. Each value can be used directly to create predicates, with the available predicate builder methods matching the value type:

  // range comparisons for date values
  Predicate isDueThisMonth = WorkObjectValues.DUE_TIME.between(startOfMonth, endOfMonth);

  // relative comparisons for numeric values
  Predicate isHighPriority = WorkObjectValues.PRIORITY.greaterThanOrEq(8);

  // wildcard comparisons for string values
  Predicate matchesWildcardName = WorkObjectValues.NAME.like("Smi*");

Predicates can be combined through using AND/OR operators, as well as negated via the NOT operator. This can be done either using the fluent predicate API or the static helper methods in the Predicates class:

  Predicate isActive = WorkObjectValues.STATE.isActive();
  Predicate assigneeJane = WorkObjectValues.ASSIGNEE_ID.eq("jane");
  Predicate candidateUserJane = WorkObjectValues.CANDIDATE_USER_IDS.containsAnyOf("jane");
  Predicate candidateGroupAdmin = WorkObjectValues.CANDIDATE_GROUP_IDS.containsAnyOf("admin");

  // fluent API to construct AND/OR combinations of two predicates
  Predicate predicate = isActive.and(assigneeJane.or(candidateUserJane));

  // alternative API to construct AND/OR combinations of any number of predicates
  Predicate otherPredicate = Predicates.and(isActive,
          Predicates.or(assigneeJane, candidateUserJane), candidateGroupAdmin);

  // fluent API to negate a predicate
  Predicate isNotCompleted =
          WorkObjectValues.STATE.isCompleted().not();

  // alternative API to negate a predicate
  Predicate isNotAssignedToJane =
          Predicates.not(WorkObjectValues.ASSIGNEE_ID.eq("jane"));

When querying for work objects using a predicate, a SearchScope may be supplied to remove some of the normal restrictions on work object access that are imposed by the framework (for example to include work objects that are outside of the current tenant or are not normally visible to the current user:

  List<WorkObject> tenants =
          this.workObjectService.findWorkObjects(
                  WorkObjectValues.TYPE.eq(WorkObjectTypes.TENANT_TYPE),
                  SearchScope.ALL_TENANTS_NO_RESTRICTIONS);

A Query wraps a predicate to define the work objects that should be matched, but also allows for paging and ordering of the results and for some parts of the work objects to be left out if they are not required. A QueryBuilder is used to assemble a query:

  // Search for the 20 oldest active tasks, omitting variable data.

  Predicate isActivePredicate =
          WorkObjectValues.SUB_STATE.eq(WorkObjectSubState.SUB_STATE_ACTIVE);

  Query query = Query.builder()
          .predicate(isActivePredicate)
          .limit(20)
          .sorting(WorkObjectValues.CREATION_TIME.orderAsc())
          .hints(WorkObjectQueryHints.OMIT_VARIABLES)
          .build();

5.2.4. Query hints

The class WorkObjectQueryHints defines query hint constants. These can mostly be used directly, for example to suppress the loading of variables in the query results.

Restricting the returned variables

The RESTRICT_VARIABLES constant is a builder for hints that restrict the returned variables returned to a limited subset. In the simplest case you can just give the plain names of the variables that you want returned:

  Query query = Query.builder()
          .predicate(WorkObjectValues.ID.eq(this.workObjectId))
          .hints(WorkObjectQueryHints.RESTRICT_VARIABLES.matching("foo"))
          .build();
Constraining the returned variables using plain string names is not currently supported for List or Map variables, and may result in incorrect results (such as empty collections). The value accessor variant is therefore preferred.

You can also restrict the returned variables using value accessors.

  static final StringValue<String> FOO = newStringValue("foo");
  // ...

      Query query = Query.builder()
              .predicate(WorkObjectValues.ID.eq(this.workObjectId))
              .hints(WorkObjectQueryHints.RESTRICT_VARIABLES.matching(FOO))
              .build();

This variant will also work for value accessors that are backed by a list value (e.g. a set):

  static final ComplexValue<Set<String>> TEST_SET =
          newSetValue("testSet", String.class);
  // ...
      Query query = Query.builder()
              .predicate(WorkObjectValues.ID.eq(this.workObjectId))
              .hints(WorkObjectQueryHints.RESTRICT_VARIABLES.matching(TEST_SET))
              .build();
It is not currently possible to restrict variables if the results should contain a map value. Attempting to restrict the results using map variable accessor will result in an unrestricted query, i.e. all variables will be returned, including the required map value.

5.2.5. Work object values

In the simplest case, work object values can simply be set and accessed using a string for the value name:

  WorkObject task = WorkObject.builder()
          .type(WorkObjectTypes.TASK_TYPE)
          .value("foo", "bar")
          .build();

  String fooValue = task.getValue("foo", String.class);

This approach has a number of disadvantages:

  • the value type is not checked when the value is written

  • only the raw work object value types are supported (as described in the package Javadoc)

  • the type has to be explicitly given everywhere that the value is accessed

  • predicates to search for this variable must be created using verbose low-level primitives

The package com.edorasware.api.value provides a number of classes that are useful when working with work object values. The WorkObjectValues class provides typed constants for the common work object values:

  String taskName = task.getValue(WorkObjectValues.NAME);

as well as static methods to create custom typed value accessors:

  static final StringValue<String> FOO = WorkObjectValues.newStringValue("foo");

      // ...

      WorkObject task = WorkObject.builder()
              .type(WorkObjectTypes.TASK_TYPE)
              .value(FOO, "bar")
              .build();

      String fooValue = task.getValue(FOO);

The different static methods correspond to the 'raw' type that will be persisted in the work object, so newStringValue() will create a typed accessor for a String value, newLongValue() will create a typed accessor for a Long value etc.

Typed value accessors can also make use of a ValueConverter to map between an application-specific data type and one of the supported 'raw' types. For example we may want to represent airport codes in application code in a type-safe way using the following class:

  /**
   * Type-safe representation of an airport code.
   */
  public class AirportCode {
      private static final Set<String> VALID_CODES =
              ImmutableSet.of("LAX", "LHR", "ZRH", "VLC");

      private final String code;

      /**
       * Constructs a new instance.
       *
       * @param code the airport code as a string value
       * @throws IllegalArgumentException if the code is not valid
       */
      public AirportCode(String code) {
          if (!VALID_CODES.contains(code)) {
              throw new IllegalArgumentException("Invalid value: " + code);
          }
          this.code = code;
      }

      /**
       * Returns the 'raw' airport code as a string value.
       *
       * @return the airport code
       */
      public String getCode() {
          return code;
      }

      // other implementation (hashcode, equals, business logic etc.)
  }

To conveniently store airport code values in a work object we can define a suitable ValueConverter implementation:

  public class AirportCodeConverter implements ValueConverter<AirportCode, String> {
      @Override
      public String serialize(AirportCode airportCode) {
          return airportCode.getCode();
      }

      @Override
      public AirportCode deserialize(String stringValue) {
          return new AirportCode(stringValue);
      }

      @Override
      public TypeToken<String> getSerializedValueType() {
          return TypeToken.of(String.class);
      }
  }

This converter can then be used to define specific typed values and use them to access AirportCode values in a type-safe way:

  static final StringValue<AirportCode> DESTINATION =
          newStringValue("destination", new AirportCodeConverter());

      // ...

      AirportCode destination = new AirportCode("ZRH");

      WorkObject task = WorkObject.builder()
              .type(WorkObjectTypes.TASK_TYPE)
              .value(DESTINATION, destination)
              .build();

      AirportCode value = task.getValue(DESTINATION);

As well as solving the problems with restricted type support and poor type checking seen with accessing values using the plain string name, the typed value constants also allow the simple creation of type-safe query predicates directly from the application type:

  AirportCode requiredDestination = new AirportCode("VLC");
  Predicate predicate = DESTINATION.eq(requiredDestination);
in addition to the common work object values defined in WorkObjectValues some type-specific value constants are defined in separate type-specific classes (e.g. DocumentValues).

6. Time provider API

The current system time in edoras one is managed by the configured TimeProvider implementation. Using a time provider across the system allows changes in the system time to be simulated (for example in unit tests) without having to wait for them to elapse in real-time.

Always use time provider to get the current time instead of calling System.currentTimeMillis() or System.nanoTime(). This makes sure that the system has a consistent view of the current time everywhere.

7. Customizing edoras one

This section describes the extension points for common use cases. If no suitable extension point is documented for a feature that you require then contact edorasware support to find a solution.

7.1. Logging configuration

If we use log4j as the logging framework then the logging configuration for log4j is provided by the log4j.properties file in the root package.

Default logging configuration in log4j.properties
  # Comment this line and uncomment the following to allow log writing to a local file
  log4j.rootLogger=INFO, A
  # log4j.rootLogger=INFO, A, local.file

  log4j.appender.A=org.apache.log4j.ConsoleAppender

  log4j.appender.A.layout=org.apache.log4j.PatternLayout
  log4j.appender.A.layout.ConversionPattern=%d{ISO8601} %-5p %-85.85c - %m%n

  log4j.appender.local.file=org.apache.log4j.FileAppender
  log4j.appender.local.file.append=false
  log4j.appender.local.file.file=/tmp/edoras.log
  log4j.appender.local.file.threshold=INFO
  log4j.appender.local.file.layout=org.apache.log4j.PatternLayout
  log4j.appender.local.file.layout.ConversionPattern=%-5p %c: %m%n

  ## Quieten distracting noise
  log4j.logger.org.springframework=WARN
  log4j.logger.com.edorasware.commons.core.maintenance.internal.jdbc.JdbcMaintenancePersistence=WARN

  ## Spring integration
  #log4j.logger.org.springframework.integration=DEBUG

  ## Spring web
  #log4j.logger.org.springframework.web=DEBUG

  ## Project
  #log4j.logger.com.edorasware.customer.acme=DEBUG

If an alternative logging configuration is needed in a given environment, this can be specified without rebuilding the WAR file by setting the appropriate system property when the application server is started, for example by adding -Dlog4j.configuration=file:C:/tmp/log4j.properties to the application server command line.

7.2. Database configuration

You can specify an alternative data source by declaring a PersistenceConfigurationConfigurer bean in your application’s Spring configuration.

The builder supplied to this bean’s configure() method allows the application data source and transaction manager to be defined. It is also possible to set a separate data source and transaction manager for database schema manipulation. This can be used environments where the default database connection does not have permissions to modify the database schema.

7.3. Web security configuration

The application web security can be configured using the following two extension points:

7.4. Mail server integration

edoras one can be configured to send mail using an external SMTP server by setting the relevant system properties as described in the edoras one Operator Guide. No additional Spring configuration is required.

If you want to configure a completely different mail sender implementation then you should use a MailConfigurationConfigurer bean declaration.

7.5. Tenant initialization

To use edoras one, you also need to create a tenant. An edoras one installation may support multiple tenants using a single database, where each tenant is completely isolated from the others (i.e. it has its own set of users, work objects, Apps etc.).

On startup, edoras one looks for tenant JSON configuration files in the locations configured by the system.tenant-cfg-location property. By default this will be the resource folder /com/edorasware/config/custom/tenant on the server’s classpath. If a configuration file is found for a tenant not already present in the database then that tenant will be initialised according to the tenant configuration file, e.g.:

Tenant initialization in acme.json
  {
      "name": "acme",

      "accounts": [
          {
              "name": "acme",
              "domain": "acme.com",
              "groups": [ "Manager" ],
              "users": [
                  {
                      "firstName": "John",
                      "lastName": "Smith",
                      "login": "john",
                      "email": "john.smith@acme.com",
                      "memberGroups": [ "Manager" ],
                      "language": "en"
                  }
              ]
          }
      ]
  }

Once a tenant is initialised it will not be updated, even if the tenant configuration file is changed at a later date.

For a full description of the tenant JSON format, please refer to the edoras one Administrator Guide.

7.6. Preinstalled Apps

Pre-installed Apps can be automatically installed into all tenants on the system by adding the ZIP files to the resource folder com/edorasware/config/custom/app in the application classpath. The location for pre-installed Apps can be changed using the system property apps.import.custom-location. If the system is started with a new version of a pre-installed App then the App will be updated automatically as long as this doesn’t conflict with changes on the local system.

For more information on working with Apps please refer to the section on <app-workflows>.

7.7. Content storage

In addition to the work object storage, edoras one also uses a content storage for work object attachments, document content etc. The content storage can be configured using standard system properties as described in the edoras one Operator Guide.

7.8. Action event listeners

The work object service notifies the rest of the application about changes to a work object via work object action listeners which are invoked whenever a work object is modified or its status is changed. Action listeners implement the interface AnyWorkObjectActionListener and declare two methods:

  • the actionWillBePerformed() method is invoked before the action is performed and the listener can modify the planned changes using the modification builder before they are written to the database

  • the actionPerformed() method is invoked after the changes have been written to the database

In both cases the event parameter passed into this method contains a description of the changes being made, which can be interpreted by the listener implementation as required.

Custom work object action event listeners can be added by implementing a class that implements the required interface, e.g.:

  package com.edorasware.acme.listeners;

  import com.edorasware.commons.core.any.support.AnyWorkObjectActionEvent;
  import com.edorasware.commons.core.any.support.AnyWorkObjectActionListener;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;

  import static com.edorasware.api.workobject.WorkObjectTypes.TASK_TYPE;

  public class LoggingTaskActionListener implements AnyWorkObjectActionListener {
      private static final Logger LOG =
              LoggerFactory.getLogger(LoggingTaskActionListener.class);

      @Override
      public void actionWillBePerformed(AnyWorkObjectActionEvent event) {
          if (!event.isEventForType(TASK_TYPE)) {
              return;
          }

          if (event.isCreationEvent()) {
              LOG.info("About to create task {}", event.getNewWorkObject().getId());
          }
      }

      @Override
      public void actionPerformed(AnyWorkObjectActionEvent event) {
          if (!event.isEventForType(TASK_TYPE)) {
              return;
          }

          if (event.isCreationEvent()) {
              LOG.info("Created task {}", event.getNewWorkObject().getId());
          }
      }
  }

and declaring a corresponding listener bean in the application’s Spring configuration file, e.g.:

  @Bean
  public LoggingTaskActionListener loggingTaskActionListener() {
      return new LoggingTaskActionListener();
  }

By default custom listeners declared in this way will have lowest precedence, so they will be executed after any built-in listeners have been executed. If the order of execution is important then this can be controlled using the standard Spring @Order annotation on the bean declaration method.

7.9. Automatic App updates

When a new tenant is initialized an App called the System App is automatically installed. This App contains the models required for correct operation of the edoras one software, and is typically loaded from the edoras one artifacts as it is strongly tied to a specific edoras one release.

It is strongly recommended that the System App be updated automatically to make sure that the latest version is active after the edoras one dependency has been updated to a newer version (which may also include a new System App).

Section Defining App development workflows describes how to configure workflows to support transfer of Apps between systems with automatic updates, and the section [incoming-adapter-lifecycle] describes how the pre-defined system app adapter can be configured to perform automatic updates of the System App.

7.10. Expressions and service beans

The powerful expression resolver used in the edoras one server supports access to arbitrary Spring bean property values and methods. This capability can be used to integrate custom Java code with process models via expressions: service beans can be written in Java and registered with the expression resolver, and the appropriate methods can then be invoked from a process model at the appropriate time. There are many potential ways to use this capability, but some typical use cases might be:

  • data initialization

  • data conversion

  • encapsulation of complex business logic

  • integration with external systems

7.10.1. Defining service beans

Service beans are simply plain Java objects registered as a Spring bean with a particular ID.

As an example, if we want to create a service to generate new task names, we can create the Java class:

TaskNameService.java
  package com.edorasware.acme.expression;

  import com.edorasware.api.expression.ExpressionBean;
  import java.util.concurrent.atomic.AtomicInteger;

  /**
   * An example service bean that allows unique task names to be created.
   */
  @ExpressionBean
  public class TaskNameService {

      protected static AtomicInteger taskCount = new AtomicInteger();

      /**
       * Returns a new task name based on the given base name.
       *
       * @param baseName the base name
       * @return the task name
       */
      public String nextTaskName(String baseName) {
          return baseName + "-" + taskCount.incrementAndGet();
      }
  }

and use this to create a corresponding Spring bean with a suitable ID, either explicitly:

  @Bean
  public TaskNameService taskNameService() {
      return new TaskNameService();
  }

or by adding an appropriate Spring annotation (e.g. @Service) to the class definition and enabling the Spring component scan for the relevant package:

  @ComponentScan("com.edorasware.acme")

To allow open access to all available Spring beans from a process expression would be a big security issue, so edoras one only allows the expression resolver to access only a limited set of beans. A specific bean can be enabled for expressions by adding the @ExpressionBean annotation to the bean class.

7.10.2. Accessing service beans

Once the bean has been defined and made accessible, it can be used by expressions within process definitions. As an example we can use the taskNameService bean to generate task names for user tasks:

serviceBean

7.10.3. Passing values using method parameters

The simplest way to pass values to a service bean method is to simply use the expression to provide the relevant values to the method call. We used this approach in the previous example when we passed in the name of the parent case:

Method invocation with method parameters
  #{taskNameService.nextTaskName(case.name)}

This approach has a number of advantages:

  • the input values are modeled, and it is therefore relatively easy for the modeler to understand what is happening

  • the same method may be used in different contexts where the source values come from different places

  • changes to variable names can be made without affecting running processes (old processes will continue to use the old variable name)

It works well for simple utility function when only a few parameters are required, but as the number of parameters increases, the method invocations become complex and therefore awkward to write and maintain.

7.10.4. Passing values using the execution context

For more complex use cases, an alternative is to provide the current Activiti execution context as a parameter to the service task method and allow the method itself to extract the information it needs and make any changes that may be required. When doing this, it is useful to extend the AbstractActivityService class, as this provides a number of useful utility methods for working with execution context, such as locating the current work object. The following example service uses the execution context to add a message to a variable in the root work object:

AcmeService.java
  /**
   * An example service bean that uses an execution context.
   */
  @Service
  @ExpressionBean
  public class AcmeService extends AbstractActivityService {

      // type-safe variable definition for the service message
      public static final StringValue<String> SERVICE_MSG = newStringValue("serviceMsg");

      public AcmeService() {
          super(false);
      }

      /**
       * Adds a service invocation message to the root work object.
       *
       * @param execution the execution context
       */
      public void invoke(DelegateExecution execution) {
          AnyWorkObject workObjectInScope = getWorkObjectInScope(execution);
          AnyWorkObject rootObject = getRootCase(workObjectInScope);

          WorkObjectUpdateBuilder updateBuilder =
                  getWorkObjectService().createUpdateBuilder(rootObject.getId());

          String message = "AcmeService was invoked at " + getTimestamp()
                  + " with tag " + getTag(execution);

          updateBuilder.value(SERVICE_MSG, message);

          updateBuilder.apply();
      }

      /**
       * Returns the value of the tag property in the task definition.
       *
       * @param execution the execution context
       * @return the value of the tag property
       */
      protected String getTag(DelegateExecution execution) {
          Map<String, String> properties = getEdorasPropertyMapForCurrentActivity(execution);
          String value = properties.get("tag");
          return hasText(value) ? value : "UNDEFINED";
      }

      /**
       * Returns a string with the current timestamp.
       *
       * @return the current timestamp
       */
      protected String getTimestamp() {
          DateTimeFormatter dateFormat =
                  DateTimeFormatter
                          .ofPattern("yyyy-MM-dd hh:mm")
                          .withZone(ZoneId.of("UTC"));

          return dateFormat.format(Instant.now());
      }
  }

The execution context is passed into the method call by using the execution keyword within the expression:

Method invocation with execution context
  #{acmeService.invoke(execution)}

This approach delegates full control to the service implementation, allowing it to navigate the work object hierarchy to read or modify whatever it needs to. Obviously, this is very powerful, but it also means that the values used by the method are not visible to the modeler, making it hard to see what is really going on. It is therefore important to try to make the behaviour as intuitive as possible and to provide good documentation for the available services to the modelers.

As the data used by the implementation is coded in Java rather then being part of the model, it is also more complicated to change the way that values are stored in the work object hierarchy. Any change to the values used by the service will take effect immediately for all processes that use that service in the future, including processes that are already running.

7.10.5. Managing database transactions in service beans

Service bean method invocations are typically executed from within a process. The process engine already manages the transactions used during process execution, and so no additional transaction handling needs to be provided by service bean implementations.

7.10.6. Security

As service beans have access to the full functionality of Java and edoras one, it is also important that security is considered when creating a new service bean implementation. Users should not be able to use a service bean to gain access to confidential information to which they would not otherwise have access, to corrupt or delete information, or to interfere with the normal functioning of edoras one.

7.10.7. Creating custom service tasks in the process palette

We have already seen how service beans can be invoked using expressions. When specific service beans are used on a regular basis, it may be convenient to extend the process palette in the process modeler to include customized service tasks for common use cases.

Details of how to customize the process palette can be found in the section [customizing-palettes].

As an example, we start by creating a patch to the default edoras one process palette (thereby reusing all of the existing palette configuration):

acme.process.palette.xml
  <palette id="acme-process-palette"
           resource-bundle="translation"
           apply-patch-to-palette="edoras-one-process-palette"
           xmlns="http://www.edorasware.com/schema/vis/palette"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.edorasware.com/schema/vis/palette http://www.edorasware.com/schema/vis/edoras-vis-process-palette-2.0.58.xsd">

      <!-- palette configuration... -->
  </palette>

To make our custom tasks easy to find, we will create a separate group to contain them:

acme.process.palette.xml
  <group id="acme-tasks">
      <component id="acmeservicetask" extends="ServiceTask"
                 attribute-groups="acmeServiceTaskAttributes"/>
  </group>

In the process modeler, this group is now added to the existing ones:

acme group

We can then define the attributes that should appear when the service task is edited. For our example, we will provide a default name and background color, set the expression to invoke the service (not visible in the modeler), define a new tag attribute, and suppress some of the additional service task attributes that are not needed:

acme.process.palette.xml
  <attribute-groups>
      <attribute-group id="acmeServiceTaskAttributes">

          <!-- Provide suitable default values for base attributes -->
          <attribute category="common" id="name" type="SimpleText"
                     value="ACME service task"
                     readonly="false" optional="true"
                     ref-to-view="text_name"/>
          <attribute id="bgcolor" category="common" type="Color"
                     value="#ccffcc" index="190"
                     readonly="false" optional="false"
                     ref-to-view="fill_el" fill="true" stroke="false"/>

          <!-- use a fixed expression to invoke the service -->
          <attribute id="expression" category="edoras" type="SimpleText"
                     value="#{acmeService.invoke(execution)}"
                     readonly="true" optional="false" visible="false"/>

          <!-- add a custom attribute that we can access in the service -->
          <attribute id="tag" category="edoras" type="SimpleText"
                     value="default" export="true"
                     readonly="false" optional="false"/>

          <!-- suppress attributes that aren't needed -->
          <attribute id="delegateExpression" category="edoras" type="SimpleText"
                     value=""
                     readonly="false" optional="true" visible="false"/>
          <attribute id="resultVariable" category="edoras" type="SimpleText"
                     value=""
                     readonly="false" optional="true" visible="false"/>
          <attribute id="class" category="edoras" type="SimpleText"
                     value=""
                     readonly="false" optional="true" visible="false"/>

      </attribute-group>
  </attribute-groups>

Note that the tag attribute is marked as 'exported', meaning that it will be added to the final task definition as a property, where it can be accessed by the service bean. The resulting definition property key has the edoras: prefix to avoid possible collisions with other properties used in the process engine.

The display strings used in the modeler are supplied by the corresponding resource bundle, allowing them to be translated:

translation.properties
  acme-tasks.title = ACME tasks
  acmeservicetask.title = ACME service task
  acmeServiceTaskAttributes.tag.title = Tag
  acmeServiceTaskAttributes.tag.description = Tag

When we add the custom task to a process in the modeler, we can see that the default attribute values are applied, and that some of the normal service task attributes are no longer available:

acme attributes

7.11. REST services

7.11.1. Custom REST API

To add your own REST API, you first need to define your own servlet context. This can be done by providing an implementation of the CustomWebApplicationInitializer interface in a sub-package of com.edorasware.config.custom:

  public class AcmeWebApplicationInitializer implements CustomWebApplicationInitializer {

      @Override
      public void preConfigureServletContext(ServletContext servletContext,
                                             WebApplicationContext rootContext)
      {
          AnnotationConfigWebApplicationContext servletAppContext =
                  new AnnotationConfigWebApplicationContext();
          servletAppContext.setParent(rootContext);
          servletAppContext.register(AcmeDispatcherConfiguration.class);

          DispatcherServlet dispatcherServlet = new DispatcherServlet(servletAppContext);
          ServletRegistration.Dynamic oneRegistration =
                  servletContext.addServlet("acme dispatcher", dispatcherServlet);

          oneRegistration.setLoadOnStartup(1);
          oneRegistration.addMapping("/acme/*");
      }

      @Override
      public void postConfigureServletContext(ServletContext servletContext,
                                              WebApplicationContext webApplicationContext) {
      }

      @Override
      public void postConfigureServletFilters(ServletContext servletContext,
                                              WebApplicationContext webApplicationContext) {
      }
  }

This specifies a request mapping context of "/acme" and also registers the dispatcher configuration class. This configuration contains a single controller, created explicitly rather than using a component scan:

  @EnableWebMvc
  @Configuration
  public class AcmeDispatcherConfiguration extends WebMvcConfigurerAdapter {

      @Bean
      public AcmeController acmeController() {
          return new AcmeController();
      }
  }

The controller is a standard REST controller implementation:

  @RestController
  public class AcmeController {

      @RequestMapping(value = {"/acme-test"}, method = RequestMethod.GET)
      @ResponseBody
      public String hello(@RequestParam(required = false, defaultValue = "World") String name) {
          return "Hello, " + name + "!";
      }
  }

The resulting REST endpoint will then be accessible under the URL acme/acme-test under the server root URL.

7.11.2. Custom REST API security

By default, custom REST endpoints defined as shown above will use the default security of the edoras one server. This is generally fine for endpoints that are accessed by forms, as the user must be logged in to access the form.

If you need an alternative security configuration, you simply need to define this in a separate configuration class:

  @Configuration
  @Order(Ordered.HIGHEST_PRECEDENCE)
  public class AcmeWebSecurityConfiguration extends WebSecurityConfigurerAdapter {

      @Autowired
      protected UserDetailsService userDetailsService;

      @Autowired
      protected PasswordEncoder passwordEncoder;

      @Override
      public void configure(WebSecurity web) throws Exception {
      }

      @Override
      protected void configure(final HttpSecurity httpSecurity) throws Exception {
              httpSecurity
                      .antMatcher("/acme/**")
                      .authorizeRequests()
                      .antMatchers(HttpMethod.OPTIONS, "/acme/**").permitAll()
                      .antMatchers("/acme/**").authenticated()
                      .and()
                      .httpBasic()
                      .and()
                      .sessionManagement().sessionFixation().none()
                      .and()
                      .csrf().disable();
      }

      @Override
      protected void configure(final AuthenticationManagerBuilder authenticationManagerBuilder)
              throws Exception
      {
          authenticationManagerBuilder
                  .userDetailsService(userDetailsService)
                  .passwordEncoder(passwordEncoder);
      }
  }
This configuration has the highest priority order set so that it has precedence over the default security config, as the default security config defines the security context for all URLs under the server root.

7.11.3. Customizing the public REST API

The public REST API servlet Spring configuration can be customized by:

Additional REST controllers can be added to the public REST API servlet by:

  • placing the controller class in a com.edorasware sub-package

  • annotating the class with @ApiController instead of @RestController

7.11.4. Managing database transactions in REST services

edoras one uses Spring Transaction Management to manage transactions.

If your REST controller methods access a database then you should add the appropriate Spring transaction annotations so that Spring can correctly manage the transactions.

You can either add the annotations to the REST controller methods directly, or to classes invoked by the REST controller.

The recommended pattern is to delegate the business logic for the REST endpoint to a separate class and annotate that class with the appropriate transaction annotations. Using this pattern, exceptions from the business logic will result in the transaction being rolled back before control returns to the REST controller, which is then free to reply to the client in any way that it likes. If transaction annotations are added directly to the REST controller then the REST controller must itself throw an exception to force a rollback of the transaction. If this is not done then partial changes may be committed inadvertently.

Wherever the annotations are added, it is important that the scope of the annotated methods is sufficient for all related operations to be grouped correctly within a single transaction.

As an example, the REST controller for the reference data service delegates calls to a ReferenceDataManager implementation:

DatabaseReferenceDataManager.java
  /**
   * Manages reference data using persistent database storage.
   */
  public class DatabaseReferenceDataManager implements ReferenceDataManager {

      @Override
      @Transactional(readOnly = true)
      public List<Domain> getDomains(String typedText) {
          // ...
      }

      // ...

Note that only the database-driven implementation needs to be annotated in this way. The alternative StaticReferenceDataManager implementation used for testing is driven by static data and therefore does not need any transaction annotations.

7.11.5. Supporting edoras form REST-based widgets

The REST APIs needed to support specific form widgets in edoras one are described in detail in the edoras one Modeler Guide.

The Dynamic Link Button widget requires a REST endpoint with a specific response (see the edoras one Modeler Guide for details). To simplify the implementation of REST endpoints to support this widget, a Java class is provided that directly supports this response (NavigationResponse):

NavigationController.java
  // ...

  /**
   * Create a navigation response for the next (oldest) task.
   *
   * @return the navigation response or NOT_FOUND if no task is available
   */
  @RequestMapping(value = "/navigation/nextTask", method = RequestMethod.GET)
  @ResponseBody
  public ResponseEntity<NavigationResponse> getNextTask() {
      LOG.debug("Requesting next task navigation");

      Query query = Query.builder()
              .predicate(WorkObjectValues.TYPE.eq(TASK_TYPE).and(WorkObjectValues.STATE.isActive()))
              .sorting(WorkObjectValues.CREATION_TIME.orderDesc())
              .limit(1)
              .build();

      AnyWorkObject task = this.anyWorkObjectService.findWorkObject(query);
      if (task == null) {
          return new ResponseEntity<>(HttpStatus.NOT_FOUND);
      }

      NavigationResponse response = NavigationResponse.get(task, ViewName.BROWSE);
      return new ResponseEntity<>(response, RestUtils.JSON_HTTP_HEADERS, HttpStatus.OK);
  }

7.12. Message bundles

The server-side message bundles can be extended or patched by providing a suitable I18NConfigurationConfigurer bean declaration:

  @Bean
  public I18NConfigurationConfigurer acmeI18NConfigurationConfigurer() {
      return (configurationBuilder) -> {
          configurationBuilder.addMessageSourceBaseNames(
                  "classpath:com/edorasware/acme/i18n/messages");
      };
  }

The base names are resolved to a message bundle containing translation resources. e.g. "classpath:com/acme/i18n/messages" will correspond to the classpath resources:

  • com/acme/i18n/messages.properties

  • com/acme/i18n/messages_de.properties

  • com/acme/i18n/messages_fr.properties

  • …​

7.13. Custom database schema and data upgrades

The current database schema and data upgrade mechanism is based on Flyway. In edoras one we use so called upgrades to manage database schema and data changes. There are two kind of upgrades: SQL and Java data upgrades.

In the following sections we discuss each of these upgrades in detail.

7.13.1. SQL database schema upgrade

If you need to create a customized SQL upgrade, then create your upgrade file (with the .sql file type) with all the needed SQL statements and save it inside the com/edorasware/commons/core/persistence/schema/${databaseType} package such that edoras one is able to retrieve and execute it. The ${databaseType} needs to be one of these values and the SQL upgrade filename must have the edoras-gear_ prefix and a proper version suffix in the following format:

edoras-gear_A.B.C.X_Small_description.sql

The filename has three main parts, the prefix, the version number and a small description of the upgrade. You need to set all these three parts such that the upgrade will be executed automatically.

The version format is A.B.C.X where A, B, C and X are numbers. The first three places in the version A.B.C are reserved for the edoras one upgrades so you can use the fourth X number for your custom database schema upgrades. So a valid SQL database schema upgrade file could look like this:

/com/edorasware/commons/core/persistence/schema/h2/edoras-gear_1.0.1.1_Small_description.sql

This upgrade will be executed after the 1.0.1 upgrade from edoras one is executed and it will have Small description as description. To check which version number you need to use please have a look at the /com/edorasware/commons/core/persistence/schema/h2/ folder inside the edoras-gear-commons library and see which versions are used by edoras one.

Migration to another database

If you want to migrate for example from Oracle 11 to Oracle 12 and you have custom database schema scripts you need to follow these steps:

  1. Retrieve the database type for the new database (in the example it would be oracle12c) from these values.

  2. Next create a new package in your project for the new database at com/edorasware/commons/core/persistence/schema/${databaseType}.

  3. Then move the custom schemas from the old database package to the new one and adapt the DDL to match the new database SQL dialect.

After that you are able to run edoras one with the your custom database schema upgrades on the new database.

7.13.2. Java data upgrade

Besides the SQL schema upgrades we also need to upgrade data from time to time. We use the same mechanism here but we do the upgrades in Java. Here you can implement a subclass of the com.edorasware.one.upgrade.OneBaseUpgrade which has an abstract upgrade(TenantId) method which you need to implement. This method gets called when Flyway executes this upgrade and it is called for each tenant once.

You can define the class as Spring bean and use the comfort of autowiring the needed beans for the data upgrade. You just need to pass the version and the description to the super class. This is similar to the version and description part in the filename of a SQL database schema upgrade. The OneBaseUpgrade class provides you with some helper methods to easily iterate over the work objects. Please have a detailed look at the class before doing the custom upgrade such that you do not duplicate the needed logic.

7.14. Elasticsearch integration

Edoras one supports flexible Elasticsearch integration. Content from the edoras one database can be mirrored into one or more Elasticsearch indices and used for custom search functionality based on the Elasticsearch query API. For some queries this may be significantly faster than running equivalent queries directly against the edoras one database.

The edoras one database is the definitive data store, and the Elasticsearch index may lag behind the database slightly, as updates have to be passed from edoras one to Elasticsearch and then indexed. If a particular use case requires definitive current data then you should retrieve this from the database not the Elasticsearch index.

This section describes how the Elasticsearch integration can be configured.

7.14.1. Set up an Elasticsearch cluster

To set up a cluster, simply follow the instructions from the Elasticsearch web page. To get started you can just download the Elasticsearch distribution (version 5.6.3), unpack it and run the start script.

You should set the Elasticsearch cluster name to edorasone (see the config/elasticsearch.yml file in the Elasticsearch distribution).

If you want to include document content from edoras one in the index then you will also need to install the mapper attachments plugin plugin (now deprecated) or the ingest attachment plugin in the Elasticsearch cluster nodes.

For small installations the default cluster settings should be sufficient. If you need more information then please refer to one of the many good resources available on Elasticsearch cluster administration. This is a complex topic that will not be covered here.

7.14.2. Elasticsearch integration dependency

Once you have a cluster running, you can configure edoras one to mirror database content to the Elasticsearch index.

The first step is to add the edoras-one-index module as a dependency to your project, e.g.:

pom.xml
  <dependency>
          <groupId>com.edorasware.one</groupId>
          <artifactId>edoras-one-index</artifactId>
          <version>${com.edorasware.one.version}</version>
  </dependency>

7.14.3. Enable Elasticsearch

To enable the Elasticsearch integration code you need to set the search.elasticsearch.enable property to true.

7.14.4. Configuration properties

The properties used to configure the Elasticsearch integration (e.g. to connect to an external Elasticsearch cluster) are described in the standard property documentation in the edoras one Operator Guide.

7.14.5. Elasticsearch index handling

Before looking at how to configure an index to be mirrored from edoras one, we need some background information on how the edoras one Elasticsearch integration manages its indices.

Working indices and aliases

Each index has a public index name (e.g. edoras-objects) which can be used by application code to access the index and perform queries, but in fact this index name is just an alias for a 'real' working index. Working indices have the the same name but with a timestamp appended (e.g. edoras-objects-20160913111617). Thus the index structure for an active edoras one index may look like this:

index structure

There may be multiple working index copies (some of which may be inactive) but there will only be one alias.

Initial synchronization and resynchronisation

edoras one will automatically check whether a valid index is available when it is started. If an index is not found then a synchronization process will be started, copying all of the required data from the edoras one database into a new working index. When this synchronization process is complete, the alias will be created so that the new working index may be accessed. In a large database this may mean that it takes some time before the index is available.

Once the synchronization is complete and the index alias has been created, further changes to the edoras one database will automatically be mirrored to Elasticsearch.

If for some reason the Elasticsearch index is out of step with the edoras one database then a resynchronization can be performed. When resynchronizing, a new working index will be created in the background and the alias will only be switched over to the new index when the synchronisation is complete. Thus the stale index can still be used up to the point where the new index is ready.

To trigger a resynchronization of all indices, the current user should have administrator permissions and access the following URL (relative to the edoras one root context):

/rest/api/v1/maintenance/index/synchronizeAll

A specific index can be resynchronized using the following URL:

/rest/api/v1/maintenance/index/{index}/synchronize
Database reset

When the database is reset (normally this is done in a development environment) then the existing indices are obviously no longer valid. In this case both the working indices and the current alias will be deleted (unless configured otherwise) and a new index will be created.

7.14.6. Index configuration

The application indexing configuration can be customized using the IndexingConfigurationConfigurer extension point.

The configuration builder allows custom IndexConfiguration implementations to be registered. Each implementation defines the structure of a custom index and provides the code needed to populate it whenever the contents of edoras one are changed:

  @Bean
  public IndexingConfigurationConfigurer acmeIndexingConfigurationConfigurer(
          DocumentAdapter documentAdapter,
          AcmeIndexEntrySerializer serializer)
  {
      return builder ->
              builder.addIndexConfigurations(
                      new AcmeIndexConfiguration(documentAdapter, serializer));
  }

  @Bean
  public AcmeIndexEntrySerializer acmeIndexEntrySerializer() {
      return new AcmeIndexEntrySerializer();
  }

You can create custom index configurations that index specific parts of the edoras one database, or index only part of the information stored in each work object.

The creation of a custom Elasticsearch index is described in the following sections.

7.14.7. Custom indices

To configure a custom Elasticsearch index, you will need the following:

  • an Elasticsearch template file describing the JSON content of your index

  • a serialization implementation to convert edoras one work objects / definitions into index JSON documents

  • an optional deserializer and DTO class to simplify handling of JSON documents from search results

  • an index configuration defining which edoras one content should be indexed and how this should be done

  • optional utility classes to simplify access to the index (e.g. by providing shared implementations of common searches, returning DTO instances rather than raw JSON documents)

7.14.8. Template file

To define the template file for your custom index, please refer to the Elasticsearch documentation. The following example defines an index with a small set of work object fields:

acme-template.json
  {
    "template": "#{index.name}*",
    "settings": {
      "number_of_shards": 1,
      "number_of_replicas": 1,
      "refresh_interval": "1s",
      "index.mapping.attachment.indexed_chars": -1
    },
    "mappings": {
      "_default_": {
        "dynamic": "strict",
        "_all": {
          "enabled": false
        }
      },
      "indexEntry": {
        "properties": {
          "id": {
            "type": "string",
            "index": "not_analyzed"
          },
          "globalId": {
            "type": "string",
            "index": "not_analyzed"
          },
          "name": {
            "type": "string",
            "index": "analyzed"
          },
          "type": {
            "type": "string",
            "index": "not_analyzed"
          }
        }
      }
    }
  }

One important point is that the template attribute contains the placeholder #{index.name} followed by a *. The placeholder will be replaced by the configured index name when the template is loaded, and the * allows this template to be used for the working indices which have the index name as prefix and a timestamp suffix. This attribute value can therefore be used in all index templates.

7.14.9. Index serialization / deserialization

The index configuration will need to convert the incoming work objects and definitions into corresponding JSON documents that match the configured template file. It may also be useful to provide a Java interface for working with the indexed JSON content. There are several ways that this can be achieved, but one possibility is to define a DTO class with the appropriate attributes that can be initialised from an edoras one entity and serialised to JSON using a JSON object mapper:

AcmeIndexEntry.java
  public class AcmeIndexEntry {

      @JsonProperty
      protected String id;

      @JsonProperty
      protected String globalId;

      @JsonProperty
      protected String type;

      @JsonProperty
      protected String name;

      protected AcmeIndexEntry() {
          // needed for JSON mapper
      }

      AcmeIndexEntry(WorkObject workObject) {
          this.id = workObject.getId();
          this.globalId = workObject.getGlobalId();
          this.type = workObject.getType();
          this.name = workObject.getName();
      }

      AcmeIndexEntry(Entity entity) {
          this.id = entity.getId();
          this.globalId = entity.getGlobalId();
          this.type = entity.getType();
          this.name = entity.getName();
      }
  ...

In this case the DTO is simple and only contains fields that are shared between work objects and definitions. We can therefore reuse the same DTO for all index content.

The DTO can also be initialized from a JSON document (as parsed by a JSON object mapper):

AcmeIndexEntry.java
  AcmeIndexEntry(JsonNode jsonNode) {
      this.id = jsonNode.findPath("id").asText();
      this.globalId = jsonNode.findPath("globalId").asText();
      this.type = jsonNode.findPath("type").asText();
      this.name = jsonNode.findPath("name").asText();
  }
A separate JSON object mapper with qualifier indexObjectMapper is provided by the elasticsearch integration code, and should be used for index serialization / de-serialization operations.

7.14.10. Custom index configuration

Custom index configurations can index all of the edoras one database or just a subset, or may index only part of the information stored in each work object. An IndexConfiguration implementation may look as follows:

AcmeIndexConfiguration.java
  public class AcmeIndexConfiguration implements IndexConfiguration {
      static final String INDEX_NAME = "acme";
      static final String DOCUMENT_TYPE = "indexEntry";

      protected final DocumentAdapter documentAdapter;
      protected final AcmeIndexEntrySerializer serializer;

      public AcmeIndexConfiguration(DocumentAdapter documentAdapter,
                                    AcmeIndexEntrySerializer serializer) {
          this.documentAdapter = documentAdapter;
          this.serializer = serializer;
      }

      // ...
Basic index setup

The IndexConfiguration interface defines a number of methods. Firstly there are some basic index setup methods (index name etc.):

AcmeIndexConfiguration.java
  @Override
  public String getIndexName() {
      return INDEX_NAME;
  }

  @Override
  public String getIndexDescription() {
      return "Acme sample index configuration";
  }

  @Override
  public Resource getTemplateResource() {
      return new ClassPathResource("acme-template.json", AcmeIndexConfiguration.class);
  }

  @Override
  public boolean deleteOnReset() {
      // if the database is reset then an existing index will also be deleted
      return true;
  }

  @Override
  public boolean requiresExplicitSynchronizationRequest() {
      // this index can be synchronized as part of a synchronizeAll operation
      return false;
  }

The migrate() method can be used to patch the template of an existing index if the template is changed at a later date. This won’t usually update the existing index information (for which a full resynchronization may be required) but it will allow the existing index to accept new JSON documents without throwing an exception. In our case we only have one template version so no migration code is required:

AcmeIndexConfiguration.java
  @Override
  public void migrate() {
      // nothing to do
  }
Indexing during normal edoras one operation

We now come to the 'normal' indexing methods that are used to mirror changes from edoras one into the index:

AcmeIndexConfiguration.java
  @Override
  public void indexWorkObject(WorkObject workObject) {
      String document = this.serializer.serialize(new AcmeIndexEntry(workObject));
      this.documentAdapter.indexDocument(
              INDEX_NAME, DOCUMENT_TYPE, workObject.getId(), document);
  }

  @Override
  public void deleteWorkObject(String workObjectId) {
      this.documentAdapter.deleteDocument(
              INDEX_NAME, DOCUMENT_TYPE, workObjectId);
  }

When a work object is created or modified, the work object will be passed to the indexWorkObject() method to be added to the index. This converts the work object to a corresponding JSON document using the serialization code described above, and adds it to the current index using the document adapter. The document adapter is just a simple wrapper around the Elasticsearch client API provided by the edoras one Elasticsearch integration code.

The indexWorkObject() method may also filter the work objects that are indexed.

work object indexing

Any work object deletions are passed to the deleteWorkObject() method so that the corresponding index entry can be removed.

A corresponding set of methods is provided to support the normal indexing of work object definitions and content manager content.

Resynchronization indexing

The resynchronization indexing process is managed by a different set of methods:

AcmeIndexConfiguration.java
  @Override
  public Optional<Predicate> getWorkObjectSynchronizationPredicate() {
      return Optional.of(Predicates.EMPTY);
  }

  @Override
  public void syncUpdateWorkObject(SyncWriter syncWriter,
                                   String workingIndexName,
                                   WorkObject workObject) {
      syncWriter.indexDocument(
              workingIndexName,
              DOCUMENT_TYPE,
              workObject.getId(),
              this.serializer.serialize(new AcmeIndexEntry(workObject)));
  }

  @Override
  public void syncDeleteWorkObject(SyncWriter syncWriter,
                                   String workingIndexName,
                                   String workObjectId) {
      syncWriter.deleteDocument(workingIndexName, DOCUMENT_TYPE, workObjectId);
  }

The getWorkObjectSynchronizationPredicate() method returns an optional predicate that is used to select the work objects that should be indexed. In this case we want to index all work objects in the system, so we return the empty predicate.

The predicates from all index configurations are combined by the framework to obtain the superset of work objects that will be synchronized. In this way each work object is only read once even though it may be needed to synchronize multiple indices. The work objects matched by the combined predicate are passed to the syncUpdateWorkObject() which should also filter the incoming work objects in the same way as normal indexing, as the combined predicate will probably return additional work objects that would not be matched by the predicate provided by the specific index configuration.

synchronization indexing

The syncUpdateWorkObject() method will contain similar code to the normal indexing method, indexWorkObject() but should update the index using the SyncWriter implementation provided. SyncWriter is optimised for the high throughput needed by index resynchronisation.

One other thing to consider in the resynchronisation code is the indexing of content manager content. For normal indexing, changes to work objects and content are handled by separate indexing methods, and so a work object index entry can be updated without having to re-index all attached content. During index synchronization there is no separate content synchronization path, so content attached to work objects should be indexed when the parent work object is indexed.

Additional index customisation

When the index is resynchronized, the customizeIndex() method will be called. This method can add additional content to the resynchronized index as required using the SyncWriter (e.g. from an external system). If you are only indexing edoras one content then nothing needs to be implemented here.

If the external content is dynamic then you may have to detect changes and add them to the index. The Elasticsearch integration framework provides no explicit support for this.

AcmeIndexConfiguration.java
  @Override
  public void customizeIndex(SyncWriter syncWriter, String workingIndexName) {
      // synchronize additional index content here
  }

7.14.11. Querying the Elasticsearch indices

You can use the indices that are created by the Elasticsearch integration to perform your own queries. Currently there is no specific support for this other than the Elasticsearch client API. It may be useful to create utility classes to support common queries, e.g.:

AcmeIndexSearch.java
  /**
   * Provides search functionality for the sample Elasticsearch index.
   */
  public class AcmeIndexSearch {

      @Autowired
      protected AcmeIndexEntrySerializer serializer;

      @Autowired
      protected Client elasticClient;

      /**
       * Finds a single index entry with the given ID.
       *
       * @param workObjectId the work object ID
       * @return the matching index entry
       */
      public AcmeIndexEntry findIndexEntry(String workObjectId) {
          List<AcmeIndexEntry> searchResults = findIndexEntries(idsQuery().addIds(workObjectId));
          if (searchResults.size() == 0) {
              return null;
          } else if (searchResults.size() == 1) {
              return searchResults.get(0);
          } else {
              throw new RuntimeException("Too many results for ID lookup");
          }
      }

      /**
       * Finds the index entries that match the given Elasticsearch query.
       *
       * @param query the Elasticsearch query
       * @return the macthing index entries
       */
      public List<AcmeIndexEntry> findIndexEntries(QueryBuilder query) {
          SearchResponse searchResponse =
                  this.elasticClient
                          .prepareSearch(AcmeIndexConfiguration.INDEX_NAME)
                          .setTypes(AcmeIndexConfiguration.DOCUMENT_TYPE)
                          .setQuery(query)
                          .get();

          return getSearchResults(searchResponse);
      }

      protected List<AcmeIndexEntry> getSearchResults(SearchResponse searchResponse) {
          return Arrays.stream(searchResponse.getHits().hits())
                  .map(input -> getSerializer().deSerialize(input.getSourceAsString()))
                  .collect(Collectors.toList());
      }

      protected AcmeIndexEntrySerializer getSerializer() {
          return serializer;
      }
  }

Elasticsearch query results may also be made available for use by form components using custom REST endpoints.

7.15. Customizing the edoras one web interface

The edoras one web interface can be customized by adding any custom Javascript code or CSS to the custom.js and custom.css files respectively. These files should be located in the root folder and will be included by the edoras one application automatically. No other changes are required to include these files.

For example we can customize the CSS to add a red box around the work item lists:

custom.css
  .thumbnailList {
     border: 1px solid red;
  }
css example

7.16. Customizing the edoras one UI

All templates have been merged into one (index.html) and now you can select which parts of the UI should be visible.

To activate the parameters you have to set them as 'true' or '1'

There are 2 master parameters, which you can combine:

  • emb: When true, the UI is configured in the same way of the old embedded.html

  • cmmn: When true, the UI is configured in the same way of the old case.html

A part from that, there are other available parameters:

  • hm (refers to Hide Menu): When true, the menu is not rendered.

  • hh (refers to Hide Header): When true, the header is not rendered.

  • ha (refers to Hide ActionsPane): When true, the actions pane is not rendered.

  • hvr (refers to Hide Version): When true, the version (in footer) is not rendered.

We have also available some useful methods in the api which you will find in: edoras.ui.configuration

The available methods are:

  • isEmbedded: Returns true when embedded is enable

  • isCmmn: Returns true when cmmn is enabled.

  • isPresent(x): Returns if 'x' is enabled when 'x' can be:

    • menu

    • header

    • cmmnHeader

    • viewContainer

    • ngView

    • cmmnNgView

    • version

    • actionPane

  • showActionsPane(boolean): Set the boolean value to actionPane

  • showMenu(boolean): Set the boolean value to menu

  • showHeader(boolean): Set the boolean value to header (it can be 'header' or 'cmmnHeader' depending on if cmmn is enabled)

  • showVersion(boolean): Set the boolean value to version

  • setIsEmbedded(boolean): Set the embedded configuration

  • setIsCmmn(boolean): Set the cmmn configuration

Some examples:
http://localhost:1339/?emb=true#/dashboard/user ==> Enables embedded configuration
http://localhost:1339/?hm=true#/dashboard/user ==> The menu is not rendered

7.17. Customizing the requested files

There are 3 parameters to manage the css and js files loaded. You can avoid the load of widgets, custom and VIS. They are loaded by default, but if you set to 'true' or '1' the right parameter you will avoid the load of them.

The available parameters are:

  • nv (refers to No Vis): When true, the js and css files of VIS won’t load

  • nw (refers to No Widgets): When true, the js and css files of Widgets won’t load

  • nc (refers to No Custom): When true, the js and css files of Custom won’t load

8. Customizing graphical modeler palettes

The graphical modeler allows the palette used to design processes, forms and cases etc. to be customized. One or more customized palettes of form, process or case widgets can be created by extending the generic widgets supported by default. Once created, each custom palette can be associated with one or more models of the appropriate type. When a custom palette has been associated with a model all the widgets from the palette will be available in the appropriate editor view.

A custom form palette has to be configured in an XML file that has the .form.palette.xml file extension. It should conform to the edoras-vis-form-palette XML schema definition.

A custom process palette has to be configured in an XML file that has the .process.palette.xml file extension. It should conform to the edoras-vis-process-palette XML schema definition.

A custom case palette has to be configured in an XML file that has the .case.palette.xml file extension. It should conform to the edoras-vis-case-palette XML schema definition.

8.1. Configuring palette locations

The locations for additional palette configurations can be set using system properties as described in the edoras one Operator Guide.

Classpath locations simplify deployment but mean that palette changes will need to be deployed by rebuilding the deployment artifact.

Filesystem locations may be system-specific but would allow palettes to be updated without rebuilding the deployment artifact. The latter may be especially useful in a development environment where palettes are being modified regularly. A filesystem location may look like this:

file:${user.home}/acme/palette

All of the custom palette XMLs located directly under the configured folder will be available and can be associated with an edoras vis model. One model can only be associated with one custom palette configuration.

If a custom palette XML is deleted or if any of its custom widgets are deleted, the widgets in models associated with that custom palette will automatically fallback to their respective generic widget parent(s) when the models are subsequently opened for editing.

8.2. Configuring a custom palette

The root element of a custom palette XML file supports the following attributes:

Table 3. Attributes and elements supported by the palette element
Name Mandatory Description

id

true

ID of the palette.

title

false

The title of custom palette that is displayed in edoras vis editor view.

parent-palette

false

The parent palette from which a custom palette is extended.

edoras vis supports the following parent process palettes:

  • edoras-gear (default value)

  • bpmn2.0

edoras vis supports the following parent form palette:

  • edoras-forms (default value)

edoras vis supports the following parent case palette:

  • case1.0 (default value)

Note that only the values shown here are supported and for normal edoras one development there is no use case that requires this attribute to be set.

This attribute does not support arbitrary creation of new palettes based on an existing palette. To create a separate palette based on existing one, you have to make a new copy and change all of the element IDs (as they must be unique).

To modify an existing palette use the apply-patch-to-palette attribute.

hide-parent-palette-elements

false

The boolean value to hide stencils of the parent palette. Defaults to true.

apply-patch-to-palette

false

The ID of a palette to be patched. The contents of the referenced palette will be modified / extended instead of a new palette being created.

resource-bundle

false

Resource Bundle File containing language specific translations. All resource bundle files should be placed under an i18n directory in the same directory as the palette XML file. Refer to Defining resource keys for language specific attributes in custom palette for more details.

See Sample custom palette for an example.

8.2.1. Defining widget groups in a custom palette

edoras vis supports grouping of widgets within custom palettes.

Table 4. Attributes supported by the group element
Name Mandatory Description

id

true

Unique Id of the group

title

false

Title of the group

description

false

Description of the group

index

false

Index of the group, Sorted in ascending order

Sample group element is shown below.

<group id="Events" title="Events" description="Events description" index="5" >
    <component id="events_start" extends="StartNoneEvent"
               attribute-groups="commonAttributes, formControlAttributes"/>
    <component id="events_end" extends="EndNoneEvent" description="End Event">
        <attribute id="custom_event_attribute" title="Custom Event Attribute" value="custom"/>
    </component>
</group>

8.2.2. Defining widgets in a custom palette

edoras vis supports creating of custom widgets in a custom palette by extending/referencing available generic widgets.

To add a widget to the custom palette, a widget element has to be added to the group element.

The widget element in turn may contain one or more attribute elements which define the widget specific properties.

Table 5. Attributes supported by the widget element
Name Mandatory Description

id

true

Unique Id of the widget

title

false

Title of the widget

description

false

Description of the widget

visible

false

Configure visibility of the widget in editor.

extends

false

Id of the widget that is extended

ref

false

Id of the widget that is referenced

roles

false

Comma separated list of roles supported by the widget, Which are inturn used by rules. See Defining rules in a custom palette for more details.

attribute-groups

false

Comma separated list of attribute group id that need to be added to the widget. See Defining attribute groups in a custom palette for more details.

presentation-id

false

Presentation reference required to render the widget SVG/Image on the editor canvas. See [defining-custom-component-presentation-in-a-custom-palette] for more details.

default-lane-id

false

Applicable only for Pool type.Configure default lane Id for a pool.

shortcut-menu-index

false

Applicable only for Process palette. Configure index for short cut menus from process widgets.

index

false

Index of the widget, sorted in ascending order

behaviour-type

false

Applicable only for Form palette. Describes the behaviour of the referred widget.

Widget extending StartNoneEvent with attribute-groups:
<component id="events_start"
           extends="StartNoneEvent"
           attribute-groups="commonAttributes, formControlAttributes" >
</component>
Widget extending Task with custom widget-presentation and short cut menu index:
<component id="formtask"
           extends="Task"
           presentation-id="presentation.task"
           shortcut-menu-index="1" >
</component>
Widget referencing EndNoneEvent with a custom attribute and index:
<component id="events_end"
           description="End Event"
           ref="EndNoneEvent"
           index="5">
    <attribute id="custom_event_attribute"
               title="Custom Event Attribute"
               value="custom"/>
</component>
Widget behaving like a password widget:
<component id="base-password"
           extends="component"
           presentation-id="password.presentation"
           behaviour-type="Password" />

8.2.3. Defining custom categories in a custom palette

edoras vis supports defining of custom categories for attributes. These categories can be used to display the attributes of a widget under different headings in the property window of the editor view.

Table 6. Attributes supported by the category element
Name Mandatory Description

id

true

Unique Id of the category

title

false

Title of the category

index

false

Index of the category. It decides the position of category in property window

visible

false

Boolean value to hide the attributes category in property window

Sample custom-categories element is shown below.

<custom-categories>
    <category id="custom_category_1" index = "101" title="custom category 1(en)" />
    <category id="custom_category_2" title="custom category 2(en)"/>
    <category id="custom_category_3" title="custom category 3(en)" visible="false"/>
</custom-categories>

Sample application of custom categories to attributes is shown below.

<attribute id="namekey"  value=""   type="SimpleText" category= "custom_category_1" />
<attribute id="name" title="Name" description="Name" value="" type="SimpleText" category="custom_category_2"/>
<attribute id="behavior" type="ComboBox" category= "custom_category_1"  index="3" title="behavior">
    <items>
        <item id="none" title="none" value="none" />
        <item id="all" title="all" value="all" />
        <item id="one" title="one" value="one" />
        <item id="complex" title="complex" value="complex" />
    </items>
</attribute>

8.2.4. Defining attributes in a custom palette

edoras vis supports defining custom attributes in the custom palette configuration.

To add an attribute with in custom palette, an attribute element has to be used. This element can only exist with in any of the following parent elements:

  • attribute-group

  • custom-attributes-group

  • model-attributes

  • component

Table 7. Attributes supported by the attribute element
Name Mandatory Description

id

true

Unique identifier of the attribute

title

false

Title of the attribute

description

false

Description of the attribute

value

false

Value of the attribute

type

false

Type of the attribute. Check Attribute types supported in a custom palette for more details.

category

false

Name of the category under which the attribute should be displayed in the property window of editor view.

export

false

Boolean value to specify whether an attribute should be exported to the xml and json(only for forms).

index

false

Integer value to define position of the attribute in the property window of the editor view.

ref-to-view

false

Id of the SVG element to which attribute value is mapped. Know more ref-to-view

readonly

false

Boolean value to make the attribute readonly.

optional

false

Boolean value to make the attribute mandatory.

visible

false

Boolean value to make the attribute visible in the property window.

filter

false

Applicable only for TreeView type. A comma separated list of file extensions. Know more filter

multilanguage

false

Boolean value to specify whether the attribute supports multilanguage. Know more multilanguage

fill

false

Applicable only for Color type. Know more fill

url

false

Applicable only for RestComboBox type.

stroke

false

Applicable only for Color type.

multiselect

false

Applicable only for RestComboBox type.

constant

false

Boolean value to make attribute value as constant.

length

false

Integer value to support maximum length for value.

popular

false

Boolean value to make attribute to has higher priority while displaying.

field-map

false

Applicable only for RestComboBox type. To map item display-name and value returned in the REST end point’s response.

item-icon-visible

false

Applicable only for RestComboBox type. To make each item’s icon visible.

select-all

false

Applicable only for RestComboBox type. To pre-select all the items returned by the REST end.

custompalette

false

Applicable only for TreeView type. To provide name of the palette for default selection in tree view editor’s drop-down list.

model-id

false

Applicable only for TreeView type.

runtime

false

Applicable for all type attributes. To make run time attribute visible. Know more runtime and runtime-value

runtime-value

false

Provide default value to run time attribute. Know more runtime and runtime-value

readonly-editor

false

Applicable only for RestComplex and Complex type. To provide non editable editor.

placeholder

false

Applicable only for TreeView type.

output-mapping

false

Applicable only for complex editor type.

edoras vis support multiple languages.

Table 8. Language specific attributes supported by the attribute element
Name Mandatory Description

title

false

If title is Resource bundle key, Then title will be picked form resource bundle file.

description

false

If description is Resource bundle key, Then description will be picked form resource bundle file

value

false

If value is Resource bundle key, Then value will be picked form resource bundle file

8.2.5. Defining resource keys for language specific attributes in custom palette

edoras vis supports defining language specific for custom attributes/widgets in the custom palette configuration.

edoras vis follows below format while generating translation keys.

Table 9. Tranlation keys for supported elements
Element Translation Key Format

Component

Component supports title and description as language specific property

<component-id>.title
<component-id>.description

Group

Group supports title and description as language specific property

<group-id>.title
<group-id>.description

Attribute

Attribute support only title, description and value as language specific property

If attribute element is within the widget element then the format of key is

<component-id>.<attribute-id>.title
<component-id>.<attribute-id>.description
<component-id>.<attribute-id>.value

If attribute element is within the attribute-goup element then the format of key is

<attribute-group-id>.<attribute-id>.title
<attribute-group-id>.<attribute-id>.description
<attribute-group-id>.<attribute-id>.value

If attribute element is within the custom-attributes-goup element then the format of key is

<custom-attributes-group-id>.<attribute-id>.title
<custom-attributes-group-id>.<attribute-id>.description
<custom-attributes-group-id>.<attribute-id>.value

If attribute element is within the model-attributes element then the format of key is

<model-attributes-id>.<attribute-id>.title
<model-attributes-id>.<attribute-id>.description
<model-attributes-id>.<attribute-id>.value

Category

Category support only title as language specific property

<category-id>.title

8.2.6. Defining validation rules for attributes in a custom palette

edoras vis supports defining validation rules for custom attributes in the custom palette configuration.

edoras vis supports following validation rules.
Table 10. Validation rules
Rule Description Example

Range

This rule can be applied to integer attributes to specify minimum and maximum allowed values.

<attribute id="cCardCvv" type="Integer"  title="Credit Card CVV" description="Credit Card CVV" optional="false">
    <validation-rules>
        <range min="100" max="999" />
    </validation-rules>
</attribute>

Length

This rule can be applied to specify the minimum and maximum length of value.

<attribute id="cCard" type="SimpleText"  title="Credit Card Number" description="Credit Card Number" optional="false">
    <validation-rules>
        <length min="0" max="16" />
    </validation-rules>
</attribute>

Expression

This rule can be applied to validate value against JavaScript expression.

<attribute id="cCard" type="SimpleText"  title="Credit Card Number" description="Credit Card Number" optional="false">
    <validation-rules>
        <expression pattern="^4[0-9]{12}(?:[0-9]{3})?$" />
    </validation-rules>
</attribute>
Named validations support in edoras vis

Refer Configuring named validations in palette section for details on named validation configuration in edoras vis.

8.2.7. Attribute types supported in a custom palette

edoras vis supports following attribute types.

Table 11. Attribute types
Type Field Type Example

SimpleText

Text Field

<attribute id="name" title="Name" description="Name" value="" type="SimpleText"/>
simpletext

TextArea

Text Area

<attribute id="summary" title="Summary" description="Summary" value="" type="TextArea" />
textarea

Integer

Number Field

<attribute id="age" title="Age" description="Age" value="15" type="Integer" category="common" />
integer

Float

Number Field

<attribute id="price" title="Price" description="Price" value="1.11" type="Float" category="common" />
float

Boolean

Check Box

<attribute id="isValid" title="Is Valid" description="Is Valid" value="true" type="Boolean"/>
boolean

ComboBox

ComboBox

<attribute id="selectnumber" title="Select Number" description="Select Number" value="2" type="ComboBox">
    <items>
        <item id="1" title="One" value="1" />
        <item id="2" title="Two" value="2" />
        <item id="3" title="Three" value="3" />
    </items>
</attribute>
combobox

Check Special attribute items for more details.

RestComboBox

ComboBox

<attribute id="company" title="Company" description="Company" url="http://www.company.com" type="RestComboBox"/>
restcombobox

TextEditor

Text Editor

<attribute id="notesShort" title="NotesShort" type="TextEditor" value="" description="Short notes."/>
texteditor

RichText

Rich Text Editor

<attribute id="documentation" title="Documentation" description="Documentation" value="" type="RichText"/>
richtext

TreeView

Tree view dialog

<attribute id="form_entry" title="FormRef" type="TreeView" value="" description="Form referenced by Form Task." filter="xfm" optional="false" readonly="true" />
treeview

For supported key event Click here [key-events-for-treeview-dialog]

Date

Date field

<attribute id="duedate" title="Due Date" description="Due Date" value="" type="Date"/>
date

ComplexDate

Date Editor

<attribute id="min-date" type="ComplexDate" value="" readonly="false" path="extraSettings.minDate" export="true" category="edoras" index="105"/>
complexdate

Color

Color field

<attribute id="bgcolor" title="BackgroundColor" type="Color" value="#ffffcc" description="" ref-to-view="fill_el" fill="true" stroke="false" optional="false"/>
color

Complex

Complex dialog

<attribute id="in" title="In" type="Complex" description="Execution element for data input." value="">
    <complex-items>
        <complex-item id="source" name="Source" type="SimpleText" value="" width="150" />
        <complex-item id="target" name="Target" type="SimpleText" value="" width="150" />
    </complex-items>
</attribute>
complex

For supported key event Click here [key-events-for-complex-dialog]

ComplexKeyValue

Complex dialog

<attribute id="in" title="In" type="ComplexKeyValue" description="Property for Complex Key Value editor." value="">
    <complex-items>
        <complex-item id="key" name="Key" type="SimpleText" value="" width="150" />
        <complex-item id="value" name="Value" type="SimpleText" value="" width="150" />
    </complex-items>
</attribute>

In the ComplexKeyValue type the first column is always taken as the key and the second column is always taken as value and any further columns are ignored even if present.

complexkeyvalue

For supported key event Click here [key-events-for-complex-dialog]

ComplexForm

Form dialog

<attribute id="email" title="Email Properties" type="ComplexForm" description="Email properties" value="">
    <complex-items>
        <complex-item id="from" name="From" type="SimpleText" value="" width="250" vtype="expressionOrEmail" optional="false" />
        <complex-item id="to" name="To" type="SimpleText" value="" width="250" vtype="expressionOrEmailList" optional="false" />
        <complex-item id="cc" name="Cc" type="SimpleText" value="" width="250" vtype="expressionOrEmailList" />
        <complex-item id="bcc" name="Bcc" type="SimpleText" value="" width="250" vtype="expressionOrEmailList" />
        <complex-item id="subject" name="Subject" type="SimpleText" value="" width="250" />
        <complex-item id="html" name="Html" type="RichText" value="" width="250" />
    </complex-items>
</attribute>
complexform

Check Special Attribute complex-items for more details.

Link

Link field

<attribute id="link" title="SimpleLink" type="Link" value="http://www.google.com" />
link

ComplexTrigger

Complex Timer dialog

<attribute id="ComplexTrigger" type="ComplexTrigger" title="Complex Trigger" value="" optional="true" readonly="false" category="edoras" />
complextrigger

RestComplex

Complex dialog

<attribute id="RestComplex" readonly-editor="false" type="RestComplex" title="RestComplex" value="" url="../rest/one-groups" readonly="false" optional="true" category="edoras">
        <complex-items>
            <complex-item id="id" type="SimpleText" value="" name="ID" optional="false" />
            <complex-item id="title" type="SimpleText" value="" name="Title" optional="true" />
            <complex-item id="value" type="SimpleText" value="" name="Value" />
        </complex-items>
</attribute>
complex

8.2.8. Defining custom widget presentation in a custom palette

edoras vis supports defining the SVG and Image for a widget.

Table 12. Attributes supported by the widget-presentations element
Name Mandatory Description

base-palette-icon-path

true

Base folder path where all related images are present.

base-editor-view-path

true

Base folder path where all related SVG file are present.

Table 13. Attributes supported by the widget-presentation element
Name Mandatory Description

id

true

Id of the widget presentation.

palette-icon-path

false

Path of image file used in presentation.

editor-view-path

false

Path of SVG file used in presentation.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<component-presentations base-palette-icon-path="icons" base-editor-view-path="view">
    <component-presentation id="presentation.number" palette-icon-path="number.png" editor-view-path="number.svg"/>
    <component-presentation id="presentation.password" palette-icon-path="password.png" editor-view-path="password.svg"/>
</component-presentations>

For detailed explanation on oryx related elements and attributes in SVG view check here SVG view features

8.2.9. Defining rules in a custom palette

edoras vis supports defining the rules for a widget behaviour.

Table 14. Rules supported by an edoras vis custom palette widgets
Name Mandatory Description

containment-rules

This rule can be applied to widgets to specify containment rules based on roles.

<containment-rules>
    <containment-rule role="base_container" contains="standard_container,core_form_controls"/>
    <containment-rule role="standard_container" contains="core_form_controls"/>
</containment-rules>

morphing-rules

This rule can be applied to widgets to specify morphing rules (Changing from one type to another) based on roles.

<morphing-rules>
    <morphing-rule role="controls_morph" base-morphs="Input"/>
</morphing-rules>

connection-rules

This rule can be applied to widgets to specify connection rules between widgets based on roles (Used Only in Process Custom Palettes).

<connection-rules>
    <connection-rule role="SequenceFlow">
        <connects from="sequence_start" to="sequence_end"/>
    </connection-rule>
    <connection-rule role="MessageFlow">
        <connects from="messageflow_start" to="messageflow_end"/>
    </connection-rule>
</connection-rules>
  1. SequenceFlow widgets can be used to connect between widgets having roles "sequence_start" and "sequence_end"

  2. MessageFlow widgets can be used to connect between widgets having roles "messageflow_start" and "messageflow_end"

cardinality-rules

This rule can be applied to widgets to specify number of incoming and outgoing connector rules based on roles (Used Only in Process Custom Palettes).

<cardinality-rules>
     <cardinality-rule role="Startevents_all">
        <incoming role="SequenceFlow" maximum="0"/>
    </cardinality-rule>
    <cardinality-rule role="Endevents_all">
        <outgoing role="SequenceFlow" maximum="0"/>
    </cardinality-rule>
</cardinality-rules>
  1. Widgets having "Startevents_all" as role cannot have incoming sequence flows.

  2. Widgets having "Endevents_all" as role cannot have outgoing sequence flows.

8.2.10. Defining attribute groups in a custom palette

edoras vis supports defining custom attribute groups, which can be used to assign a set of attributes to multiple widgets. .Attributes supported by an attribute-group element

Name

Mandatory

Description

name

true

Unique name of the attribute group

An attribute-groups element can contain one or more attribute-group elements. Similarly an attribute-group element can contain one or more attribute elements, as shown in the sample below.

<attribute-groups>
    <attribute-group id="commonAttributes">
        <attribute id="id" title="Id" value=""/>
        <attribute id="nameKey" title="NameKey" description="NameKey" type="SimpleText" value="" />
    </attribute-group>
    <attribute-group id="formControlAttributes">
        <attribute id="label" runtime="false" multilanguage="false" category="common"/>
        <attribute id="description" title="Description" type="SimpleText" visible="false"/>
    </attribute-group>
</attribute-groups>

if an attribute-group with same id present across multiple palettes, it will extend those attributes as well. If the attribute-group with same id contains repeated attribute(i.e with same id) across multiple custom palettes, it will show warning. Similarly if repeated attributes differs in default value, it will show warning.

8.2.11. Defining custom attributes in custom palette

edoras vis supports defining custom attributes.

Table 15. Attributes supported by the custom-attributes-group element
Name Mandatory Description

id

true

Id of the custom-attributes-group.

ref

false

Id of the widget to which the custom attributes are applied to. If this attribute is skipped then the attributes are applied to all the widgets.

A custom-attributes element can contain one or more custom-attributes-group elements. Similarly, a custom-attributes-group element can contain one or more attribute elements, as shown in the sample below.

<custom-attributes>
    <custom-attributes-group id="customAttributeGroup1">
        <attribute id="id" title="Id" value=""/>
        <attribute id="nameKey" title="NameKey" description="NameKey" type="SimpleText" value="" />
    </custom-attributes-group>
    <custom-attributes-group id="customAttributeGroup2" ref="Task">
        <attribute id="label" runtime="false" multilanguage="false" category="common"/>
        <attribute id="description" title="Description" type="SimpleText" visible="false"/>
    </custom-attributes-group>
</custom-attributes>

8.2.12. Defining model attributes in a custom palette

edoras vis supports defining model attributes in a custom palette.

To add model attributes a model-attributes element has to be added.

A model-attributes element can contain one or more attribute elements. All attributes which are defined within the model-attributes element will be added to the model. See the sample below.

<model-attributes id="modelAttributes" >
    <attribute id="expressionid" title="Label Expresion" value=""/>
    <attribute id="guid" title="GUID" description="GUID of the model" type="SimpleText" value="" />
</model-attributes>

8.3. Special attributes of attribute element in palette

8.3.1. ref-to-view

ref-to-view specifies an id of SVG element in the graphical representation of a widget. If this attribute is set, the property will manipulate the graphical representation at run-time, for example, changing the color or rendering text. Depending on the property’s type you can reference different types of SVG elements as show below.

  • ref-to-view="text_name" (for text)

  • ref-to-view="fill_el" (for color)

<attribute id="name"   value="" category="common"  type="SimpleText" ref-to-view="text_name"/>

8.3.2. filter

The filter attribute is applicable only for TreeView type and is used to filter the nodes displayed in the tree view.

edoras vis supports following filters :

  • mod is used for Processes

  • xfm is used for Forms

<attribute id="form_entry" title="FormRef" type="TreeView" value="" description="Form referenced by Form Task." filter="xfm" optional="false" readonly="true" />

8.3.3. fill

fill is an optional attribute applicable only for Color type. If fill attribute is set to true the background color of a shape can be set.

fill

8.3.4. multilanguage

multilanguage is an optional attribute used to show or hide the language specific attributes for a given attribute in the property window.

multilanguage

8.3.5. runtime and runtime-value

runtime is an optional attribute used to show or hide runtime-value of an attribute in the property window of the editor.

<attribute id ="description" runtime-value="" runtime="true" multilanguage="true"> </attribute>
runtime

8.3.6. wrapLines

wrapLines is an optional attribute used to customize attributes of type String.

If wrapLines is set to false, the text field shown will be of single line.

If wrapLines is set to true, the text field shown will be of multi line.

8.3.7. items

items can be used as a child element of the attribute element of type ComboBox. This is used to define the list of values for the combobox.

<attribute id="behavior" type="ComboBox" category= "edoras"  index="3" title="behavior">
    <items>
        <item id="none" title="none" value="none" />
        <item id="all" title="all" value="all" />
        <item id="one" title="one" value="one" />
        <item id="complex" title="complex" value="complex" />
    </items>
</attribute>

8.3.8. complex-items

complex-items can be used as child element of attribute element if the attribute is of one of the following types:

  • Complex

  • ComplexForm

complex-items element can contain one or more complex-item elements.

Table 16. Attributes supported by the complex-item element
Name Mandatory Description

id

true

Id of the complex-item

name

false

Title of the complex-item

value

false

Value of the complex-item

type

false

Type of the complex-item, It can only support following types:

  • SimpleText

  • TextArea

  • ComboBox

  • RestComboBox

  • Boolean

  • Integer

  • Float

  • Date

  • ComplexDate

  • Color

  • Link

  • TextEditor

  • RichText

  • TreeView

vtype

false

A validation type for validating values, the supported validation types are:

  • singleEmail

  • emailList

  • expressionOrEmail

  • expressionOrEmailList

width

false

Width of the complex-item

optional

false

Used to make the complex-item optional or otherwise

<attribute id="email" title="Email Properties" type="ComplexForm" description="Email properties" value="">
    <complex-items>
        <complex-item id="from" name="From" type="SimpleText" value="" width="250" vtype="expressionOrEmail" optional="false" />
        <complex-item id="to" name="To" type="SimpleText" value="" width="250" vtype="expressionOrEmailList" optional="false" />
        <complex-item id="cc" name="Cc" type="SimpleText" value="" width="250" vtype="expressionOrEmailList" />
        <complex-item id="bcc" name="Bcc" type="SimpleText" value="" width="250" vtype="expressionOrEmailList" />
        <complex-item id="subject" name="Subject" type="SimpleText" value="" width="250" />
        <complex-item id="html" name="Html" type="RichText" value="" width="250" />
    </complex-items>
</attribute>

8.3.9. dependency

dependency can be used as a child element of the attribute element. This is used to make attribute visible/hide based on another attribute value.

<attribute id="thumbnail-max-height" type="SimpleText" title="Thumbnail Maximum Height" value="" >
    <dependency ref="preview" value="thumbnail" />
</attribute>

8.4. Configuring named validations in palette

edoras vis supports named validations which can be used to implement customized attribute validations at design time.

A typical named validation palette configuration would look like below:

<component id="prime-number" ref="base-text" title="Prime number">
    <attribute id="value" type="SimpleText">
        <validation-rules>
            <rule name="isPrimeNumber">
                <param name="value" value="[[value]]"/>
            </rule>
        </validation-rules>
    </attribute>
    <attribute id="label"/>
</component>

For the above isPrimeNumber named validation rule configuration in the palette, vis editor will try to execute a java-script function by name isPrimeNumber by passing the value of the value attribute as a parameter.

function isPrimeNumber(num){
    if(num && !isNaN(num)){
        var N = parseInt(num);
        for(var i = 2; i <= (N/2); i++){
            if(N % i == 0){
                return false;
            }
        }
        return true;
    }
    return "Invalid value";
}

If such a function is defined and it returns one of true, false or an error message, as shown in the above sample, the validation is successful only when the return value is true and fails when the return value is either false or any other value. A warning message is shown against the attribute when the validation fails. In cases where a error message is returning the error message is shown as part of the warning message against the attribute.

This java-script function can also invoke a REST end-point (synchronously) in cases where validation needs to be done on the server.

Such a function can be added to the custom.js file already made available in the edoras-one-client module.

It is also possible to directly provide a REST end-point URL in a named validation configuration as shown in the below sample:

<component id="zip-code" ref="base-text">
    <attribute id="country" type="ComboBox" value="CH" category="common" index="2">
        <items>
            <item id="US" value="US" title="United States"/>
            <item id="CH" value="CH" title="Switzerland"/>
            <item id="DE" value="DE" title="Germany"/>
            <item id="FR" value="FR" title="France"/>
            <item id="ES" value="ES" title="Spain"/>
            <item id="IN" value="IN" title="India"/>
        </items>
    </attribute>
    <attribute id="value" type="SimpleText">
        <validation-rules>
            <rule name="validate-zip-code" url="../rest/validate-zip">
                <param name="zip" value="[[value]]"/>
                <param name="countryCode" value="[[country]]"/>
            </rule>
        </validation-rules>
    </attribute>
    <attribute id="label"/>
</component>

Even in this case the vis editor will try to execute a java-script function by name validateZipCode by passing the values of the value and country attributes as parameters.

Only if such a function is not defined, the REST URL is invoked with the specified parameters.

The REST end-point is expected to return a String value as shown in the below sample.

@ResponseBody
@RequestMapping(value = "/validate-zip", method = GET)
public String validateZipCode(@RequestParam String zip, @RequestParam(required = false) String countryCode) {

    if (countryCode != null && zip != null) {
        if (zip.length() > 3) {
            switch (countryCode) {
                case "US":
                    return String.valueOf(zip.matches("^[0-9]{5}(?:-[0-9]{4})?$"));
                case "CH":
                    return String.valueOf(zip.matches("^[0-9]{4}"));
                case "ES":
                case "FR":
                case "DE":
                    return String.valueOf(zip.matches("^[0-9]{5}"));
                case "IN":
                    return String.valueOf(zip.matches("^[0-9]{6}"));
                default:
                    return "true";
            }
        } else {
            return "Zip code too short.";
        }
    }

    return "Invalid input.";
}

The validation is successful if the REST end-point returns true and fails if it either returns false or any string other than true. A warning message is shown against the attribute when the validation fails. In cases where a error message is returned the error message is shown as part of the warning message displayed against the attribute.

8.5. SVG view features

8.5.1. oryx:magnet

With oryx:magnet you can define special points on a node where you can dock other nodes or edges to connect them.

You can connect a docker to any point on a node, but magnets help the user creating nicer looking models. If you do not define a magnet for a node, it will a have default magnet in the center. Magnets are specified with 'magnet' elements, for example:

<oryx:magnets>
    <oryx:magnet oryx:cx="1" oryx:cy="20" oryx:anchors="left" />
    <oryx:magnet oryx:cx="1" oryx:cy="40" oryx:anchors="left" />
    <oryx:magnet oryx:cx="1" oryx:cy="60" oryx:anchors="left" />

    <oryx:magnet oryx:cx="25" oryx:cy="79" oryx:anchors="bottom" />
    <oryx:magnet oryx:cx="50" oryx:cy="79" oryx:anchors="bottom" />
    <oryx:magnet oryx:cx="75" oryx:cy="79" oryx:anchors="bottom" />

    <oryx:magnet oryx:cx="99" oryx:cy="20" oryx:anchors="right" />
    <oryx:magnet oryx:cx="99" oryx:cy="40" oryx:anchors="right" />
    <oryx:magnet oryx:cx="99" oryx:cy="60" oryx:anchors="right" />

    <oryx:magnet oryx:cx="25" oryx:cy="1" oryx:anchors="top" />
    <oryx:magnet oryx:cx="50" oryx:cy="1" oryx:anchors="top" />
    <oryx:magnet oryx:cx="75" oryx:cy="1" oryx:anchors="top" />

    <oryx:magnet oryx:cx="50" oryx:cy="40" oryx:default="yes" />
  </oryx:magnets>

The oryx:magnets element is a direct child of the svg element.

magnet

The magnets are shown as small light red circles light magnet

8.5.2. oryx:docker

dockers are the other part of the dockers-magnets concept. A docker is a control object to connect an edge to a node or in this case to connect two nodes. A node can have at most one docker that can be defined by using a docker element:

<oryx:docker oryx:cx="16" oryx:cy="16" />
docker

A docker is shown as a small green circle green docker

A docker element needs only position information. Docking nodes on nodes can be used as a shortcut for connecting two nodes with an edge. In this figure, the rectangular shape is the source and the circular shape is the target.

8.5.3. Text

Text is rendered with SVG text elements.

<text
       font-size="12"
       id="text_name"
       x="50"
       y="40"
       oryx:align="middle center"
       oryx:fittoelem="text_frame"
       stroke="black">
</text>
text

Oryx extends these element with attributes for the alignment and the rotation of the text. The alignment uses the specified coordinates (attributes x and y) as the reference point. A value of middle center means that the horizontal center and vertical middle point of the text will be positioned on the reference point.

If you want to set the text element’s value using a property, you have to set the id of the text element by using ref-to-view attribute in custom palette.
<attribute id="name"   value="" category="common"  type="RichText" ref-to-view="text_name"/>

8.5.4. oryx:minimumSize

  • Value: float float

  • Initial: 1 1

  • Optional: yes

  • Applies to: SVG elements

minimumSize defines the minimum size the node can be resized to, if the node is resizable. The first value defines the minimum width, the second the minimum height.

<g pointer-events="fill" oryx:minimumSize="200 80" >

8.5.5. oryx:maximumSize

  • Value: float float

  • Initial: -

  • Optional: yes

  • Applies to: SVG elements

maximumSize defines the maximum size the node can be resized to, if the node is resizable. The first value defines the maximum width, the second the maximum height.

<g pointer-events="fill" oryx:minimumSize="80 60" oryx:maximumSize="200 160" >

8.6. Removing widgets from the main palette using fragment/patch palettes

edoras vis supports defining model attributes in a custom palette.

This feature can be used in cases where some of the default widgets provided in the default palettes should not been shown (since similar custom widgets have been added). To remove certain widgets, add the widget IDs as a comma separated list to the stencil-ids attribute of the <remove-stencils> element in the patch/fragment palette as shown in the below sample:

    <remove-stencils
            stencil-ids="cloud-attachment,cloud-upload"/>

Note this only works for elements contained in the main palette to which the patch palette is applied to.

8.7. Sample custom palette

A sample custom palette is given below:

<?xml version="1.0" encoding="UTF-8"?>
<palette id="custom-process-palette"
     xmlns="http://www.edorasware.com/schema/vis/palette"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://www.edorasware.com/schema/vis/palette http://www.edorasware.com/schema/vis/edoras-vis-process-palette-2.0.57.xsd"
     resource-bundle="translation">

    <attribute-groups>
        <attribute-group id="baseAttributes">
            <attribute id="id" value="" title="" />
            <attribute id="name" value="" title="Name"
                       multilanguage="false" category="common" ref-to-view="text_name"/>
            <attribute id="documentation" value="" title="Documentation"
                       multilanguage="false" category="common"/>
        </attribute-group>

        <attribute-group id="usertask">
            <attribute id="notesShort" title="" value="" />
            <attribute id="notesShortKey" title="" value="" />
            <attribute id="candidateusers" title="CandidateUsers" value=""
                       category="edoras" type="RestComboBox" url="/rest/modeler-users"/>
            <attribute id="candidategroups" title="CandidateGroups" value=""
                       category="edoras" type="RestComboBox" url="/rest/modeler-groups"/>
            <attribute id="support" title="Support Link" type="Link"
                       value="http://www.edorasware.com/support" />
        </attribute-group>
    </attribute-groups>

    <model-attributes id="modelAttributes">
        <attribute id="guid" value="" title="" />
        <attribute id="labelExpression" value="" title="" />
        <attribute id="version" value="" title="" />
        <attribute id="author" value="" title="" />
        <attribute id="language" value="" title="" visible="false"/>
        <attribute id="namespaces" value="" title="" visible="false"/>
    </model-attributes>

    <group title="Custom Tasks" id="custom-shapes">
        <component id="CustomFormTask" extends="FormTask"
                   attribute-groups="baseAttributes,usertask"/>
        <component id="CustomManualTask" extends="ManualTask"
                   attribute-groups="baseAttributes,usertask"/>
        <component id="CustomEndNoneEvent" extends="EndNoneEvent"
                   attribute-groups="baseAttributes"/>
        <component id="CustomSequenceFlow" extends="SequenceFlow"
                   attribute-groups="baseAttributes"/>
        <component id="CustomTextAnnotation" extends="TextAnnotation"
                   attribute-groups="baseAttributes"/>
        <component id="CustomAssociationUndirected" extends="Association_Undirected"
                   attribute-groups="baseAttributes"/>
        <component id="CustomExclusiveDatabasedGateway" extends="Exclusive_Databased_Gateway"
                   attribute-groups="baseAttributes"/>
        <component id="CustomParallelGateway" extends="ParallelGateway"
                   attribute-groups="baseAttributes"/>
    </group>

    <group title="Activities" id="activities">
        <component ref="Task" id="RefTask"/>
        <component ref="Subprocess" id="RefSubprocess"/>
        <component id="RefFormTask" extends="FormTask">
            <attribute id="formInit" value="" title="Init Form Ref"
                   type="TreeView" filter="xfm" category="common" export="true"/>
            <attribute id="guid-ref" value="" title="Init Form Ref (ID)"
                   type="SimpleText" category="common" export="true" visible="false"/>
        </component>
        <component ref="CollapsedSubprocess" id="RefCollapsedSubprocess"/>
        <component ref="ExpandedSubProcess" id="RefExpandedSubProcess"/>
        <component ref="CallActivity" id="RefCallActivity"/>
    </group>

    <group title="Gateways" id="gateways">
        <component ref="Exclusive_Databased_Gateway" id="RefExclusive_Databased_Gateway"/>
        <component ref="ParallelGateway" id="RefParallelGateway"/>
    </group>

    <group title="Events" id="events">
        <component ref="StartNoneEvent" id="StartNoneEvent"/>
        <component ref="EndNoneEvent" id="EndNoneEvent"/>
    </group>

    <group title="Connectors" id="connectors">
        <component ref="SequenceFlow" id="RefSequenceFlow"/>
        <component ref="Association_Undirected" id="RefAssociation_Undirected"/>
        <component ref="Association_Unidirectional" id="RefAssociation_Unidirectional"/>
        <component ref="MessageFlow" id="RefMessageFlow"/>
        <component ref="Association_Bidirectional" id="RefAssociation_Bidirectional"/>
    </group>

</palette>

9. Defining App development workflows

9.1. App development overview

edoras one supports configurable app development workflows, allowing automatic or semi-automatic exchange of Apps between different edoras one installations and tenants.

To transfer an App out of an edoras one tenant, the App content is written to the local filesystem using an outgoing channel adapter. The adapter can then send a message to a Spring Integration channel to trigger additional processing (e.g. to copy the App to another system).

Spring Integration can also be used to receive incoming Apps and send them to the Incoming App Service. This documentation will describe the interface between edoras one and Spring Integration. For details of how to configure Spring Integration to implement a given App distribution workflow please refer to the {spring-int-page}.

Here is a high level overview of the App workflow processing:

AppWorkflow

9.2. Outgoing adapter configuration

An outgoing adapter can be declared as a Spring bean which implements the interface:

com.edorasware.cloud.core.transfer.OutgoingAppChannelAdapter

Beans declared with this interface will automatically be detected by the edoras one Spring configuration - no additional registration is required. edoras one provides the following standard channel adapter implementations:

com.edorasware.cloud.core.transfer.internal.LocalDirectoryOutgoingAppChannelAdapter

writes the App to the local filesystem as a file tree within a root directory.

com.edorasware.cloud.core.transfer.internal.LocalZipFileOutgoingAppChannelAdapter

writes the App to the local filesystem as a ZIP file.

In all channel adapter implementations by default the App will be written using an ID that is constant across all systems where the App is installed. But you are able to configure this with the exportedAppNameExpression property on the channel adapters. The given expression is evaluated with the App Model as context, meaning that an expression like name will evaluate to the App Model name. Expression prefix and suffix like #{, } is added automatically to the expression string.

As an example, a typical outgoing channel adapter configuration for deployed Apps could look like the following:

  @Bean
  public OutgoingAppChannelAdapter localZipFileOutgoingAppChannelAdapter(
          WorkObjectService workObjectService,
          CurrentTenantService currentTenantService,
          TenantLookupService tenantLookupService,
          TimeProvider timeProvider,
          GearExpressionResolver gearExpressionResolver,
          AppExporter appExporter,
          AppWriterFactory writerFactory,
          MessageChannel loggingChannel,
          @Value("${transfer.outgoing.location}") Path basePath)
  {
      return new LocalZipFileOutgoingAppChannelAdapter(
              workObjectService,
              currentTenantService,
              tenantLookupService,
              timeProvider,
              gearExpressionResolver,
              appExporter,
              writerFactory,
              "Local Directory",
              basePath,
              loggingChannel);
  }

  @Bean
  public MessageChannel loggingChannel() {
      LoggingHandler loggingHandler = new LoggingHandler(LoggingHandler.Level.WARN.name());
      loggingHandler.setShouldLogFullMessage(true);

      DirectChannel directChannel = new DirectChannel();
      directChannel.subscribe(loggingHandler);
      return directChannel;
  }

This configuration will save each deployed App as a Zip file in the given directory, and then send a message to the logging channel (where the full message content will be logged).

9.3. Outgoing adapter message format

By setting the messageChannel property outgoing channel adapter implementations can send a message to a Spring Integration channel whenever an App is saved. These messages can be used to trigger the additional processing needed for App distribution, and have the following format:

  • the message payload is a org.springframework.core.io.Resource describing the App location

  • the message has additional headers with information that may be useful in subsequent processing

Table 17. Outgoing Spring Integration message headers
Header Type Description

appName

String

the current App name (this may change over time)

appSourceId

String

the App Global ID (constant across all systems)

comment

String

a comment supplied when the App distribution was triggered

sourceTenantName

String

the name of the originating tenant

9.4. Incoming adapter configuration

Spring Integration can be configured to import an App by sending a message to the pre-defined App channel resourceAppChannel. This channel accepts messages with org.springframework.core.io.Resource payloads describing the App location and supports:

  • remote App Zip resources

  • local App Zip files

  • local App file trees

To define incoming App workflows Spring Integration should be enabled by adding the @EnableIntegration annotation to the project configuration class. As part of the example implementation we will also make use of Spring Integration’s Java DSL, so we add the relevant dependency:

  <dependency>
          <groupId>org.springframework.integration</groupId>
          <artifactId>spring-integration-java-dsl</artifactId>
          <version>1.2.2.RELEASE</version>
  </dependency>

An incoming channel adapter configuration to automatically load apps from a given input directory could look like the following. First we start with an App source that generates an array of resources by checking for modified files that match the given pattern:

  @Bean
  public MessageSource customIncomingAppSource() {
      ResourceRetrievingMessageSource source =
              new ResourceRetrievingMessageSource("file:/tmp/INCOMING/*.zip");
      source.setFilter(new ModifiedFilesFilter());
      return source;
  }

We then connect this source via a polling adapter to the customAppArrayChannel which will receive messages containing an array of App resources. The source will be polled once a minute, checking for newly-arrived Apps:

  @Bean
  public DirectChannel customAppArrayChannel() {
      return MessageChannels.direct().datatype().get();
  }

  @Bean
  public SourcePollingChannelAdapterFactoryBean customIncomingAppAdapter() {
      SourcePollingChannelAdapterFactoryBean factoryBean
              = new SourcePollingChannelAdapterFactoryBean();

      PollerMetadata pollerMetadata = Pollers.fixedRate(1, TimeUnit.MINUTES).get();
      factoryBean.setPollerMetadata(pollerMetadata);
      factoryBean.setSource(customIncomingAppSource());
      factoryBean.setOutputChannelName("customAppArrayChannel");
      factoryBean.setAutoStartup(false);

      return factoryBean;
  }

The poller is not auto-started as we need to wait for the rest of the system to be initialized, otherwise the channel adapter may be started before edoras one is ready to receive Apps. The channel bean reference should be registered with edoras one which will start the channel at the appropriate time. To do this we overwrite the IncomingAppChannelAdapterLifecycleBean bean as follows:

  @Bean
  public static IncomingAppChannelAdapterLifecycleBean incomingChannelManager(
          IncomingAppService incomingAppService,
          List<AppDeploymentConfiguration> appDeploymentConfigurations,
          SourcePollingChannelAdapter customIncomingAppAdapter)
  {
      IncomingAppChannelAdapterLifecycleBean lifecycleBean =
              new IncomingAppChannelAdapterLifecycleBean(
                      incomingAppService,
                      appDeploymentConfigurations);

      lifecycleBean.setManagedChannels(Collections.singletonList(customIncomingAppAdapter));
      return lifecycleBean;
  }

As a final step, we create an integration flow to connect the channel for the resource arrays to the resourceAppChannel used by the edoras one server to receive incoming Apps. This splits the resource arrays into individual resource messages, and adds a header to each one to remove the original file once the import has been completed:

  @Bean
  public StandardIntegrationFlow customIncomingAppFlow() {
      return IntegrationFlows.from(customAppArrayChannel())
              .split()
              .enrichHeaders(ImmutableMap.of("removeAfterImport", true))
              .channel("resourceAppChannel")
              .get();
  }

9.5. Incoming App service message format

The incoming App service accepts messages from a Spring Integration channel as follows:

  • the message payload is a org.springframework.core.io.Resource describing the App location

  • the message has additional headers with information to control the App import process

Table 18. Incoming Spring Integration message headers
Header Type Default Description

isSystemApp

Boolean

false

Set to true if the incoming app is the system app

isEditableApp

Boolean

true

Set to true if the app should be editable after import

removeAfterImport

Boolean

false

Set to true if the incoming app should be removed after import

forceAppImport

Boolean

false

Set to true if the incoming app should always be installed (no version checking will be performed)

tenantNames

String

<empty>

A comma-separated list of tenant names for the tenants where the app should be installed. If no tenant is specified then the app will be installed on all tenants.

9.6. App version checking

When an App is imported into edoras one, a version check will normally be performed. This makes sure that the currently installed version is a direct ancestor of the version that is being imported. If this is not the case then the App being imported is based on a different version of the App than the one that is installed. This means that changes currently installed on the system may be lost.

This version check may be disabled using the incoming message headers, but this should only be done when the scenarios where this may be necessary have been established and the possible consequences of silently ignoring version conflicts have been considered.

9.7. Spring Integration extensions

The following implementations are provided by edoras one and can be used in Spring Integration configurations to support some common integration use cases:

com.edorasware.cloud.core.transfer.integration.ModifiedFilesFilter

a CollectionFilter<Resource> implementation that tracks incoming files and accepts files that are either new or have been modified. This is useful for polling an incoming App directory where the Apps aren’t removed after import.

10. Deployment

10.1. Deploying to Cloud Foundry

This appendix gives step by step instructions on how to deploy your application to a Cloud Foundry server. Cloud Foundry is an open Platform as a Service (PaaS) infrastructure.

10.1.1. Maven support

Cloud Foundry provides a Maven plugin that allows you to deploy applications with Maven goals. This appendix only gives basic instructions, please refer to http://docs.cloudfoundry.org/buildpacks/java/build-tool-int.html to learn more about Maven support for Cloud Foundry.

Install Maven plugin

To install the Cloud Foundry Maven plugin, add the corresponding section to your pom.xml file:

Cloud Foundry plugin installation in pom.xml
  <plugin>
          <groupId>org.cloudfoundry</groupId>
          <artifactId>cf-maven-plugin</artifactId>
          <version>1.0.3</version>
  </plugin>

Cloud Foundry does not have a persistent file system (the file system is cleared before an application restart), therefore it is not possible to persistently store work item and document content data in the file system. In this environment the application uses Cloud Foundry services to persistently store data.

Usually the application loads edorasware license from the file system (e.g. from the user’s home directory). Again, this approach is not possible with Cloud Foundry as there is no persistent file system, so we supply the edorasware license as an environment variable.

Configure Maven plugin

The configuration section of the plugin defines the default values used for the Cloud Foundry goals. It is possible to overwrite the default values with system variables.

server

Used to look up the Cloud Foundry credentials in the Maven settings.xml file. By default the settings.xml file is stored in ~/.m2/settings.xml. See http://maven.apache.org/settings.html#Servers to learn more about the Maven settings.xml file.

target

Defines the URL of your Cloud Foundry infrastructure API.

org

Defines your organization inside the Cloud Foundry infrastructure.

space

Defines the space to be used inside your organization.

env

Defines the environment variables needed by edoras one. The only environment variable we define here is the edorasware license.

services

Defines the Cloud Foundry services needed by edoras one: a relational database named datadb that stores the work items and a mongo DB named contentdb that stores the document content.

Putting all this together, the Cloud Foundry plugin section looks as follows:

Cloud Foundry plugin configuration in pom.xml
  <plugin>
          <groupId>org.cloudfoundry</groupId>
          <artifactId>cf-maven-plugin</artifactId>
          <version>1.0.3</version>
          <configuration>
                  <server>${cloudfoundry.server}</server>
                  <target>${cloudfoundry.target}</target>
                  <org>${cloudfoundry.org}</org>
                  <space>${cloudfoundry.space}</space>
                  <memory>1024</memory>
                  <env>
                          <EDORASWARE_LICENSE>${edorasware.license}</EDORASWARE_LICENSE>
                  </env>
                  <services>
                          <service>
                                  <name>datadb</name>
                                  <label>${cloudfoundry.service.datadb.label}</label>
                                  <plan>${cloudfoundry.service.datadb.plan}</plan>
                          </service>
                          <service>
                                  <name>contentdb</name>
                                  <label>${cloudfoundry.service.contentdb.label}</label>
                                  <plan>${cloudfoundry.service.contentdb.plan}</plan>
                          </service>
                  </services>
          </configuration>
  </plugin>

To ease the handling of the Cloud Foundry plugin configuration in the pom.xml file you can use Maven properties. The property values are then stored in two places: properties that are not security critical are defined inside a properties element at the top of the pom.xml file, security critical properties are defined in the Maven settings.xml file.

Properties in pom.xml
  <!-- Cloud Foundry properties -->
  <cloudfoundry.server>run.pivotal.io</cloudfoundry.server>
  <cloudfoundry.target>http://api.run.pivotal.io</cloudfoundry.target>
  <cloudfoundry.org>edorasware</cloudfoundry.org>
  <cloudfoundry.space>development</cloudfoundry.space>

  <cloudfoundry.service.datadb.label>elephantsql</cloudfoundry.service.datadb.label>
  <cloudfoundry.service.datadb.plan>turtle</cloudfoundry.service.datadb.plan>

  <cloudfoundry.service.contentdb.label>mongolab</cloudfoundry.service.contentdb.label>
  <cloudfoundry.service.contentdb.plan>sandbox</cloudfoundry.service.contentdb.plan>

Make sure that the server id in the Maven settings.xml file matches the server configuration value of Cloud Foundry plugin (run.pivotal.io in our case).

Settings in Maven settings.xml
     <servers>
         <server>
             <id>run.pivotal.io</id>
             <username>user</username>
             <password>pass</password>
         </server>
  </servers>

10.1.2. Use Maven plugin

To access your Cloud Foundry infrastructure, the Cloud Foundry Maven plugin offers different Maven goals. The most important is the one to deploy your application:

mvn cf:push

Congratulations! You have deployed your first edoras one application to a Cloud Foundry infrastructure.

10.1.3. Command line support

The recommended way to work with Cloud Foundry is to use the Maven integration (see above) or the Gradle integration. However for special cases it is possible to use the Cloud Foundry command line tool. One such special case is when your Cloud Foundry infrastructure does not support recent versions of the Maven / Gradle integration plugins.

It is not possible to define environment variables that contain line feeds with the Cloud Foundry command line tool. Therefore you cannot use an environment variable to supply the edorasware license to the Cloud Foundry infrastructure.

In this case you have to place the edorasware license file inside your application and access it from there, e.g. in the WEB-INF folder.

First make sure to install the correct version of the Cloud Foundry command line tool that matches your Cloud Foundry infrastructure. Then open a command line and invoke the Cloud Foundry commands as required. The following batch file lists the commands needed to deploy an edoras one application:

Batch file
  REM stop application
  cf stop edoras-one-bootstrap

  REM deploy application without start
  cf push edoras-one-bootstrap ^
     -d beta.swisscloud.io ^
     -m 1G ^
     -p target\edoras-one-bootstrap.war ^
     -t 150 ^
     --no-start

  REM create and bind relational database service
  cf create-service mariadb 512mb datadb
  cf bind-service edoras-one-bootstrap datadb

  REM create and bind document database service
  cf create-service mongodb default contentdb
  cf bind-service edoras-one-bootstrap contentdb

  REM start application
  cf start edoras-one-bootstrap

See http://docs.cloudfoundry.org/devguide/#cf to learn more about the Cloud Foundry command line tool.

10.2. How to integrate edoras one in your own html

It’s possible to use edoras one in your own html inside a div.

First of all, you should know some limitations:

  • edoras one app must be the unique angularJS app in your html.

  • The div which contains edoras one requires a defined height, that is, not auto, with percentage or pixels.

  • The minimum size supported in edoras one is 1024x768. That means that the div which contains edoras one must fit this measure.

The following html code must be inserted in your page:

<div ng-app="oneModule" class="edoras-one" >
    <script type="text/javascript" src="./forms/css/resources.css"></script>
    <script type="text/javascript" src="./one/css/resources.css"></script>
    <script type="text/javascript" src="./one/app/one.files.js"></script>
    <link type="text/css" media="all" href="./one/less/main.less" rel="stylesheet/less"/>
    <link rel="stylesheet" href="./forms/libs/upload/css/external/bootstrap-image-gallery.min.css" />
    <link rel="stylesheet" href="./forms/libs/upload/css/jquery.fileupload-ui.css" />
    <script type="text/javascript" src="./one/libs/moved/less/less-1.7.5.min.js"></script>
    <link rel="stylesheet" type="text/css" href="widgets.css"/>
    <script type="text/javascript" src="widgets.js"></script>
    <script type="text/javascript" src="widgets-module.js"></script>
    <link rel="stylesheet" type="text/css" href="custom.css"/>
    <script type="text/javascript" src="custom.js"></script>
    <div e-application-loader>
        <div id="mainDiv">
            <div e-menu></div>
            <div e-header></div>
            <div class="viewContainer">
                <div class="ng-view"></div>
                <div e-actions-pane></div>
            </div>
            <div e-version ></div>
        </div>
        <div e-notification-bar></div>
        <div e-select-dashboard-pane ></div>
        <div e-global-message></div>
    </div>
</div>

You must have the production files in the correct directory structure with "./one" and "./forms" directories and the backend endpoints.

If you want and you are able to do, you can define these css and scripts tags in your head element instead of the body, in order to be loaded synchronously before the page is rendered.

10.2.1. Customizing edoras one elements

You can avoid showing some elements like the menu, the header or the actions pane by simply deleting it from the html above. These elements are:

<div e-menu></div>
<div e-header></div>
<div e-actions-pane></div>

10.2.2. Placing our html in a different place than the resources

In the html we can set the BACK_END_BASE_URL and the FRONT_END_BASE_URL variables to specify where the server (back end) and resources (front end) are.

For example if we have our frontend resources in http://domain.com/resources but our html is in http://domain.com/web, we can set the FRONT_END_BASE_URL to "../resources/":

<script>window.FRONT_END_BASE_URL = '../resources/';</script>

If we have our backend resources in http://domain.com/rest but our html is in http://domain.com/, we can set the BACK_END_BASE_URL to "rest/":

<script>window.BACK_END_BASE_URL = 'rest/';</script>

This script tag must be placed before the one.files.js script tag:

<script>window.BACK_END_BASE_URL = '../resources/';</script>
<script type="text/javascript" src="./forms/app/one.files-1.5.0.S70.js"></script>

We can set the BACK_END_BASE_URL and FRONT_END_BASE_URL with relative or absolute values:

<script>window.BACK_END_BASE_URL = '../resources/';</script>
<script>window.BACK_END_BASE_URL = '/resources/';</script>
<script>window.BACK_END_BASE_URL = 'http://domain.com/resources';</script>
If you want to set an external domain as your context, then you need to follow the instructions in the next section about CORS. Note that currently you can only move the HTML to another context/domain and the front end resources still need to be served from the back end server. The complete separation will be done in a later release with the goal of having the front end resources together such that you have the possibility to serve these on a web server and have the back end as a pure REST server.

10.2.3. CORS configuration

To configure the back end to send the correct CORS headers you can define the following system properties:

Table 19. CORS configuration
System property Default Description

security.cors.path

/**

A path into the application that should handle CORS requests. Exact path mapping URIs (such as /admin) are supported as well as Ant-stype path patterns (such as /admin/**).

security.cors.allowed-methods

*

Comma-separated list of HTTP methods to allow, e.g. GET, POST. The special value * allows all method.

security.cors.allowed-headers

*

Comma-separated list of headers that a pre-flight request can list as allowed for use during an actual request. The special value of * allows actual requests to send any header.

security.cors.allowed-origins

*

Comma-separated list of origins to allow, e.g. http://domain1.com, http://domain2.com. The special value * allows all domains.

security.cors.allow-credentials

true

Whether user credentials are supported.

security.cors.exposed-headers

<empty>

Comma-separated list of response headers other than simple headers (i.e. Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma) that an actual response might have and can be exposed.

security.cors.max-age

1800

How long, in seconds, the response from a pre-flight request can be cached by clients.

10.3. Application logging

An edoras one project should log correctly using application-specified logging without changes.

For JBoss application servers, an additional deployment configuration file is required to prevent logging-related classes from the application container being added to the application classpath and interfering with the application-specific logging:

JBoss deployment configuration WEB-INF/jboss-deployment-structure.xml
  <?xml version="1.0" encoding="UTF-8"?>
  <jboss-deployment-structure>
      <deployment>
      <exclusions>
            <module name="org.apache.log4j"/>
            <module name="org.slf4j"/>
            <module name="org.jboss.logging"/>
            <module name="commons-logging"/>
            <module name="org.apache.commons.logging"/>
            <module name="org.jboss.logging.jul-to-slf4j-stub"/>
      </exclusions>
      </deployment>
  </jboss-deployment-structure>

Application logging is configured as described in the section Logging configuration.

10.4. Container logging

A common logging configuration uses log4j and a local logging configuration within the deployed WAR file. The resulting logs will typically be written to an application-specific log file, separate from any log files that may be used by the container in which the application is deployed.

An alternative logging approach is to delegate the logging to the container, allowing the logs from both the container and the application to be written to a single log file and controlled using a single logging configuration. The following sections describe the required configuration for this setup using some common application containers.

10.4.1. Tomcat container logging

The Tomcat application server uses a custom logging implementation for server logging that is not supported by an slf4j adapter. For this reason we cannot simply delegate the logging to the container in the default configuration. To use container logging, the server must be reconfigured to use log4j as described on the following web page:

When the server has been reconfigured in this way, you just need to remove the log4j library from the project by excluding it explicitly when the slf4j-to-log4j adapter is included. The slf4j-to-log4j adapter will then find the log4j implementation from the Tomcat lib directory and use it for all edoras one logging. You will also need to remove the project log4j.properties file, and transfer any required edoras one logging configuration to the container log4j.properties file.

If you have unit tests in your project then you will also need to add a testing runtime dependency for the log4j library, as the unit tests will not have access to the log4j library in the Tomcat container.

Maven dependency configuration
Tomcat container logging configuration in the Maven pom.xml
  <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-log4j12</artifactId>
          <version>${org.slf4j.version}</version>
          <scope>runtime</scope>
          <exclusions>
                  <exclusion>
                          <groupId>log4j</groupId>
                          <artifactId>log4j</artifactId>
                  </exclusion>
          </exclusions>
  </dependency>

  <dependency>
          <groupId>log4j</groupId>
          <artifactId>log4j</artifactId>
          <version>1.2.17</version>
          <scope>test</scope>
  </dependency>
Gradle dependency configuration
Tomcat container logging configuration in the Gradle build.gradle
  // Route the logs to log4j ...
  runtime("org.slf4j:slf4j-log4j12:$slf4jVersion") {
    // ... where log4j itself is supplied by the Tomcat container
    exclude group: 'log4j', module: 'log4j'
  }

  // we still need a local log4j library for testing
  testRuntime "log4j:log4j:1.2.17"

10.4.2. JBoss container logging

The JBoss application server supports all of the logging frameworks used in edoras one, so to use JBoss container logging it is only necessary to do the following:

  • remove the logging dependencies

  • remove the julReroute bean definition from the application’s Spring configuration

  • remove the logging configuration file log4j.properties (or move it to the test source tree if it is still needed for unit testing)

  • remove the file WEB-INF/jboss-deployment-structure.xml

If the project contains unit tests, however, the logging dependencies will need to be retained and moved to the test scope as shown below.

Maven dependency configuration
JBoss container logging configuration in the Maven pom.xml
  <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>jul-to-slf4j</artifactId>
          <version>${org.slf4j.version}</version>
          <scope>test</scope>
  </dependency>
  <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>jcl-over-slf4j</artifactId>
          <version>${org.slf4j.version}</version>
          <scope>test</scope>
  </dependency>
  <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-log4j12</artifactId>
          <version>${org.slf4j.version}</version>
          <scope>test</scope>
  </dependency>
Gradle dependency configuration
JBoss container logging configuration in the Gradle build.gradle
  // Retain test dependencies for unit testing ...
  testRuntime "org.slf4j:jul-to-slf4j:$slf4jVersion"
  testRuntime "org.slf4j:jcl-over-slf4j:$slf4jVersion"
  testRuntime "org.slf4j:slf4j-log4j12:$slf4jVersion"