1. Development environment setup

You can adopt one of theese configurations:

  • Eclipse SDK and local Cobalt server;

  • Cobalt Docker image and the IDE you prefer.

Other optional requirements are:

  • a local or remote Methode Editorial instance;

  • PostgreSQL 10.5 or above

  • Swing aligned with the same year and month version of Cobalt release

1.1. Installing and configuring Maven

You will need Maven, a dependency management and build automation tool for Java projects.

Download maven from https://maven.apache.org/ and follow installalations instruction at https://maven.apache.org/install.html.

It may be usefull to append the full path to the bin folder of the unpacked maven distribution to your PATH environment variable.

Find the .m2 folder in your user directory (e.g. C:\Users\name.surname\.m2) and create a settings.xml file with the following content, replacing repository-url with the maven repository url provided by EidosMedia:

<?xml version="1.0" encoding="UTF-8"?>
<settings>
    <mirrors>
        <mirror>
            <id>nexus</id>
            <name>Nexus central repository</name>
            <url>repository-url</url>
            <mirrorOf>*</mirrorOf>
        </mirror>
    </mirrors>
    <profiles>
        <profile>
            <id>nexus</id>
            <repositories>
                <repository>
                    <id>central</id>
                    <url>http://central</url>
                    <releases>
                        <enabled>true</enabled>
                    </releases>
                    <snapshots>
                        <enabled>true</enabled>
                    </snapshots>
                </repository>
            </repositories>
            <pluginRepositories>
                <pluginRepository>
                    <id>central</id>
                    <url>http://central</url>
                    <releases>
                        <enabled>true</enabled>
                    </releases>
                    <snapshots>
                        <enabled>true</enabled>
                    </snapshots>
                </pluginRepository>
            </pluginRepositories>
        </profile>
    </profiles>
    <activeProfiles>
        <activeProfile>nexus</activeProfile>
    </activeProfiles>
</settings>

An optional servers xml node, with authentication information for the nexus server could be needed:

<settings>
...
    <servers>
        <server>
            <id>nexus</id>
            <username>username</username>
            <password>password</password>
        </server>
    </servers>
</settings>

To avoid overriding the default local repository in the .m2 directory, an optional localRepository node could be added:

<settings>
...
    <localRepository>/path/to/your/local/repository</localRepository>
</settings>
You can use a different settings.xml if you don’t want to modify the default one in the .m2 directory, if you are using the Eclipse IDE. This is explained in the related section of this documentation.

1.2. Maven Archetypes

A set of maven archetypes is available to build server side extensions on top of the Cobalt Server:

  • Site Service Extension (com.eidosmedia.portal:archetype.web-base.extensions): extensions library (jar) archetype for the Site Service module

  • Directory Service Extension (com.eidosmedia.portal:archetype.directory.extensions): extensions library (jar) archetype for the Directory Service Module

  • Publication Service Notifier Extension(com.eidosmedia.portal:archetype.psn.extensions): custom publication notifications handler

  • Standalone Web Module (com.eidosmedia.portal:archetype.web-module): standalone web application (war) archetype with shared Cobalt libraries

1.2.1. Conventions for group and artifact names

When creating new java projects with maven, you are supposed to define a groupId and an artifactId. It is good to follow the guidelines provided on the Maven website to pick a consistent groupId and artifactId for your project.

For example in case of EidosMedia Cobalt projects, the prefix com.eidosmedia.cobalt should be used for the groupId. In case of a customer specific extension a domain controlled by the customer could be used as a prefix instead.

1.2.2. Using archetypes from comman line

You can bootstrap new project using the archetypes from command line. For example:

mvn archetype:generate -DarchetypeGroupId=com.eidosmedia.portal -DarchetypeArtifactId=archetype.web-base.extensions -DarchetypeVersion=3.2019.03

1.2.3. Using archetypes in Intellij IDE

From the New Project menu, select Maven, toggle "Create from archetype" and select "Add Archetype…​"

intellij-archetypes
Figure 1. Create from archetype in Intellij

1.2.4. Using archetypes in Eclipse IDE

From the New Project menu, select Maven, "Maven Project", continue till the "Select an Archetype" window, select the local catalog, you created in the previous section, then select the archetype and hit "Next".

eclipse-archetype
Figure 2. Create from archetype in Eclipse

1.3. Postgres

Cobalt uses PostgreSQL as its main database. If you run a monolith (with all the services in the same installation) Cobalt instance locally, you will need a postgres to connect to.

1.3.1. Running Postgres with Docker

You can easily start a preconfigured Postgres instance with Docker.

docker login dockerhub.eidosmedia.com

docker run -d --name=postgres \
           -p 5432:5432 dockerhub.eidosmedia.com/postgres

1.3.2. Using your Postgres installation

If you installed postgres already, you need to configure it, to work with Cobalt.

Modify these settings of your Postgres installation by editing the postgresql.conf file:

max_prepared_transactions = 20
default_transaction_isolation = "repeatable read"

Open the psql console and run these commands, to create the default "cobalt" schema, user and password:

postgres=# CREATE ROLE cobalt WITH PASSWORD 'cobalt' LOGIN;
postgres=# CREATE DATABASE cobalt WITH OWNER = cobalt;
postgres=# \connect cobalt;
cobalt=# CREATE SCHEMA cobalt;
cobalt=# ALTER ROLE cobalt in database cobalt SET search_path='cobalt';
cobalt=# GRANT ALL PRIVILEGES ON SCHEMA cobalt TO cobalt;

You should modify the default user and password by editing the following line of the cobalt.properties file inside the conf directory of your cobalt distribution:

bitronix.resource.ds1.driverProperties.user=cobalt
bitronix.resource.ds1.driverProperties.password=cobalt
bitronix.resource.ds1.driverProperties.databaseName=cobalt
bitronix.resource.ds1.driverProperties.serverName=localhost
bitronix.resource.ds1.driverProperties.portNumber=5432

You can also modify the hostname and port to reflect the ones of your Postgres instance.

1.4. Connecting to a remote Cobalt instance

You can connect your local development server to a remote Cobalt instance to use production data.

Edit the server.xml file in your local Cobalt, removing:

<Server port="${tomcat.port.server}" shutdown="SHUTDOWN">
    ...
    <Listener className="com.eidosmedia.portal.bitronix.PortalBitronixLifecycleListener" /> <!-- COBALT -->
    ...
</Server>

and replacing the repository JNDI resource with the following one:

<GlobalNamingResources>
    ...
    <Resource
        name="repository"
        factory="com.eidosmedia.portal.generic.services.CustomServiceFactory"
        factoryType="com.eidosmedia.portal.repository.service.JNDIRemoteRepositoryDiscoverableFactory"
        serviceDiscoveryRegistryRef="ServiceDiscoveryRegistry"
        coreServicesHandlerType="com.eidosmedia.portal.repository.restclient.RemoteRepositoryServiceHandler"
        type="com.eidosmedia.portal.repository.commons.interfaces.RepositoryServiceProvider"
        />
    ...
</GlobalNamingResources>

The server.xml is located:

  • Directly in your server configuration, if you are working in Eclipse IDE

  • conf/server.xml inside the root of the cobalt-dist package

  • conf/server.xml inside the root of the cobalt-base package, if you use the cobalt-home/cobalt-base deployment

  • /cobalt-dist/conf/server.xml inside the development Cobalt container

As last step, you need to check that the name of the repository in the remote server cobalt.properties, is correctly referenced in your local cobalt.properties, or eventually edit it accordingly:

repository.name=repo-local

1.4.1. Configuring the remote services manually

You need your local server to connect to remote Cobalt services. The easiest way to configure it, is manually registering the url in the cobalt/serviceDiscoveryRegistry.xml file:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE Configuration>
<Configuration>
  <ServiceDiscoveryRegistry>
    <ServiceInfos>

        <ServiceInfo id="cobalt-repo" domain="${common:common.defaults.domain}" zone="${common:common.defaults.zone}" type="repository" uri="http://my-remote-server:8480/repository.rest" repository="${common:repository.name}" />
        <ServiceInfo id="cobalt-directory" domain="${common:common.defaults.domain}" zone="${common:common.global.zone}" type="directory" uri="http://my-remote-server:8480/directory" realm="${common:common.defaults.realm}" />
        <ServiceInfo id="cobalt-cobaltpub" domain="${common:common.defaults.domain}" zone="${common:common.defaults.zone}" type="cobaltpub" uri="http://my-remote-server:8480/cobaltpub" />

    </ServiceInfos>
  </ServiceDiscoveryRegistry>
</Configuration>

In the snippet above, we registered the remote repository.rest, cobaltpub, and directory service remote instances.

To avoid running also locally the services, it is better to remove the corresponding context configurations from the conf/Catalina/localhost folder.

In many cases we will be developing mainly the site service, so we can comment out or delete all the xml files, except ROOT.xml

The serviceDiscoveryRegistry.xml is located:

  • Directly in your server configuration folder at the path conf/cobalt/serviceDiscoveryRegistry.xml, if you are working in Eclipse IDE

  • conf/cobalt/serviceDiscoveryRegistry.xml inside the root of the cobalt-dist package

  • conf/cobalt/serviceDiscoveryRegistry.xml inside the root of the cobalt-base package, if you use the cobalt-home/cobalt-base deployment

  • /cobalt-dist/conf/cobalt/serviceDiscoveryRegistry.xml inside the development Cobalt container

1.5. Docker

A docker container is available for development. You can login to the docker repository dockerhub.eidosmedia.com with your credentials provided by EidosMedia and pull the container:

docker login dockerhub.eidosmedia.com
docker pull dockerhub.eidosmedia.com/cobalt:3.2019.03

Running the container locally is as simple as:

docker run --name cobalt \
           -p 8480:8480 \
           -p 8843:8843 \
           -e POSTGRES_HOST={postgres-host} \
           dockerhub.eidosmedia.com/cobalt:3.2019.03

replacing {postgres-host} with the actual postgres host.

1.5.1. Using configuration properties

You can set any configuration properties available in the cobalt.properties file, through environment variables. For example, to specify the postgres host, instead of POSTGRES_HOST variable, you could use :

docker run --name cobalt \
           -e bitronix.resource.ds1.driverProperties.serverName={postgres-host} \
           ...
           dockerhub.eidosmedia.com/cobalt:3.2019.03

1.5.2. Exposing configuration, data and source directories

To expose the configuration, data, and source directories, just mount the corresponding directories, onto the directories you prefer on your host. At the first run, the container will synchronize the folders, if it founds them empty or partially empty (the priority is given to files on the host).

docker run --name cobalt \
           -v <your-conf-folder>:/cobalt-dist/conf \
           -v <your-src-folder>:/cobalt-dist/src \
           -v <your-data-folder>:/cobalt-dist/data \
           dockerhub.eidosmedia.com/cobalt:3.2019.03

1.5.3. Running a subset of cobalt services

When you are developing you will usually run only the service you need to extend, or only the site service in case you are developing themes or front-end in general:

docker run --name cobalt \
           -e COBALT_SERVICES=site
           dockerhub.eidosmedia.com/cobalt:3.2019.03

1.5.4. Debugging the container

To start cobalt in debug mode:

docker run --name cobalt \
           -p 8480:8480 \
           -p 8843:8843 \
           -p 10020:10020 \
           dockerhub.eidosmedia.com/cobalt:3.2019.03 cobalt.sh run-debug

you can then attach in debug to the socket opened at port 10020.

1.5.5. Downloading configuration files

It is possible to download (from a remote web server) and replace any configuration file in the /cobalt-dist/conf/cobalt directory (in this version not in subdirectories), by declaring CONF_ prefixed environment variables with the . character replaced with _. For example for serviceDiscoveryRegistry.xml the variable has to be CONF_serviceDiscoveryRegistry_xml:

docker run --name cobalt \
           -e CONF_serviceDiscoveryRegistry_xml=http://localhost/serviceDiscoveryRegistry.xml \
           dockerhub.eidosmedia.com/cobalt:3.2019.03

1.5.6. Development docker-compose

A docker-compose file, preconfigured to connect to a remote cobalt instance is available at:

version: '3'

services:
  cobalt:
    image: dockerhub.eidosmedia.com/cobalt:3.2019.03
    ports:
      - "80:8480"
      - "10020:10020" # JPDA port for remote debugging
    volumes:
      - ./conf:/cobalt-dist/conf
      - ./src:/cobalt-dist/src
      # Mount the the site service extension jars to be deployed in /cobalt-dist/extra/lib/site folder as in this example
      - /path/to/site/service/extension.jar:/cobalt-dist/extra/lib/site/extension.jar
      # Mount the the directory service extension jars to be deployed in /cobalt-dist/extra/lib/directory folder as in this example
      - /path/to/directory/service/extension.jar:/cobalt-dist/extra/lib/directory/extension.jar
    environment:
      - COBALT_SERVICES=site # enable also directory, if you need to develop extensions for it
      - COBALT_REMOTE_REPOSITORY=enabled
      - repository.name=repo-local # must be the same repository name of the remote server
    command: ["cobalt.sh", "run-debug"]

In order to attach to the correct remote server, you need to edit the conf/cobalt/serviceDiscoveryRegistry.xml file, replacing the right URLs of the server for each service you need to attach to:

<ServiceInfo id="cobalt-repo"
    domain="${common:common.defaults.domain}"
    zone="${common:common.defaults.zone}"
    type="repository"
    uri="[http|https]://[hostname]:[port]/repository.rest"
    repository="${common:repository.name}" />
<ServiceInfo id="cobalt-directory"
    domain="${common:common.defaults.domain}"
    zone="${common:common.global.zone}"
    type="directory"
    uri="[http|https]://[hostname]:[port]/directory"
    realm="${common:common.defaults.realm}" />

In the docker-compose.yml you need also to configure the same repository name of the remote server:

environment:
    - repository.name=repo-local

You need to configure the compose to start only the services you need to extend, for example:

environment:
    - COBALT_SERVICES=site,directory

You can develop and then deploy your extensions by mounting the jars in the corresponding folder.

For directory service extensions:

volumes:
    - /path/to/directory/service/extension.jar:/cobalt-dist/extra/lib/directory/extension.jar

For site service extensions:

volumes:
    - /path/to/site/service/extension.jar:/cobalt-dist/extra/lib/site/extension.jar

1.6. Working in the IntelliJ IDE

Better way of developing Cobalt extensions with the IntelliJ IDE, is using the Docker plugin and configuring a remote debugging to a dockerized instance of cobalt.

1.6.1. Installing the Docker plugin

Navigate to IntelliJ Preferences, select Plugins and Install JetBrains plugin…​:

install-docker-plugin
Figure 3. Install Docker Plugin

Search for Docker in the next window and install the Docker Integration plugin:

install-docker-plugin-2
Figure 4. Install Docker Integration

1.6.2. Create a new Artifact for your project

This part of the guide assumes one already has an extensions project in the IntelliJ workspace, generated from one of the archetypes described in the Maven Archetypes section.

The project output consists in a JAR file, containing extensions for one of the Cobalt services (e.g. Site Service), to be deployed in the corresponding extensions folder.

Configure a new Artifact creation task, by going into the Project Structure in the File menu:

project-structure
Figure 5. Project Structure

Select Artifacts and hit the + button. Create a new one, selecting From modules with dependencies:

create-artifact
Figure 6. Create Artifact

Click OK in the next menu:

finalize-artifact-creation
Figure 7. Finalize Artifact Creation

Override the artifact location to a folder of your choice, if needed. The default is the out/artifacts/<project-name>_jar/ folder from the root of your project.

artifact-location
Figure 8. Artifact Location

1.6.3. Docker Run/Debug Configuration

On the top-right corner of the IDE, select the Edit Configurations menu button:

new-configuration
Figure 9. New Configuration

Hit the + button to create a new Docker→Docker-compose configuration:

new-docker-configuration
Figure 10. New Docker Configuration

In the Compose file(s) input, reference the docker-compose.yml file from the Development docker-compose section.

In the Before launch section select Build Artifacts and, then, the artifact created in the previous section.

build-artifacts
Figure 11. Build Artifacts
select-artifact
Figure 12. Select Artifact

This will ensure that any time the docker-compose environment is re-started, an updated JAR file is built.

Remember to change the reference to the remote Cobalt instance, if needed, as exaplained in Development docker-compose section.

Remember to change the volume mount in the docker-compose file, to mount the jar file in right extension folder:

  • /cobalt-dist/extra/lib/site/ for Site Service extensions

  • /cobalt-dist/extra/lib/directory for Directory Service extensions

volumes:
    # Mount the the site service extension jars to be deployed in /cobalt-dist/extra/lib/site folder as in this example
    - /path/to/site/service/extension.jar:/cobalt-dist/extra/lib/site/extension.jar
    # Mount the the directory service extension jars to be deployed in /cobalt-dist/extra/lib/directory folder as in this example
    - /path/to/directory/service/extension.jar:/cobalt-dist/extra/lib/directory/extension.jar

The newly created configuration can be selected from the drop-down menu on the top-right corner of the workspace and started by hitting "play" icon.

1.6.4. New debug configuration

By default, the docker-compose file listens to the 10020 port. A remote debug configuration can attach to that port and debug directly from the IntelliJ IDE.

On the top-right corner of the IDE, select the Edit Configurations menu button, and create a new Remote Debug configuration, edit the port to be 10020 and select your project in the Select sources using module’s classpath drop-down:

remote-debug-configuration
Figure 13. Remote debug configuration
remote-debug-configuration-2
Figure 14. Remote debug configuration 2

With the docker-compose setup already running, you can attach in debug by selecting and starting the new remote debug configuration from the drop-down menu on the top-right corner of the workspace.

1.7. Working in the Eclipse IDE

A set of plugins to ease development in the Eclipse IDE is available.

1.7.1. Installing the Eclipse plugins

You need to use the Install New Software…​ feature and install the plugins from the following update sites:

install-cobalt-sdk
Figure 15. Install Cobalt SDK Plugin

1.7.2. Downloading the Cobalt Distribution

You should download the com.eidosmedia.portal:cobalt:3.2019.03:dist artifact from the repository provided by EidosMedia, picking the latest version available, and unpack it in a directory of you choice.

1.7.3. Creating a new Cobalt Server

From the File → New → Other menu select Server and click next.

new-server
Figure 16. New Server

Select Cobalt Server 1.0 in the EidosMedia S.p.A. folder, name it as you prefer and click next.

new-server-2
Figure 17. Cobalt Server

Select Browse…​ to link the server to Cobalt distribution downloaded before.

new-server-3
Figure 18. Cobalt SDK Folder

On the Project Explorer window you can see the Server configuration files copied from the cobalt-dist. You can edit this files without impacting the original installation.

If you double click on the newly create server in the Servers window you can edit the Cobalt’s data and source folder.
cobalt-server-configuration
Figure 19. Cobalt Server Configuration

Since you will develop extensions only on a few services, is better to disable the ones you don’t need, to reduce the server startup time.

You need to delete or comment out, by replacing the .xml extension with anything else, the contexts, of the underlying Tomcat, you don’t want to start. These contexts are located in the folder conf/Catalina/localhost:

disabling-services
Figure 20. Disabling unused services

1.7.4. Importing your local maven configuration in the Eclipse IDE

Eclipse, by default, uses the maven settings.xml in the ${user.home}/.m2 directory of your system. If you want to use a different configuration file open Eclipse preferences and go to to Maven→User Settings window, and set the file accordingly in the User Settings input form:

settings-maven-eclipse
Figure 21. Eclipse maven configuration

1.7.5. Importing the archetype-catalog in the Eclipse IDE

Create the following file in a directory of your choice:

<archetype-catalog xsi:schemaLocation="http://maven.apache.org/plugins/maven-archetype-plugin/archetype-catalog/1.0.0 http://maven.apache.org/xsd/archetype-catalog-1.0.0.xsd"
    xmlns="http://maven.apache.org/plugins/maven-archetype-plugin/archetype-catalog/1.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <archetypes>
    <archetype>
      <groupId>com.eidosmedia.portal</groupId>
      <artifactId>archetype.directory.extensions</artifactId>
      <version>3.2019.03</version>
      <description>archetype.directory.extensions</description>
    </archetype>
    <archetype>
      <groupId>com.eidosmedia.portal</groupId>
      <artifactId>archetype.web-base.extensions</artifactId>
      <version>3.2019.03</version>
      <description>archetype.web-base.extensions</description>
    </archetype>
    <archetype>
      <groupId>com.eidosmedia.portal</groupId>
      <artifactId>archetype.web-module</artifactId>
      <version>3.2019.03</version>
      <description>archetype.web-module</description>
    </archetype>
  </archetypes>
</archetype-catalog>

Replace the versions with the version of cobalt you downloaded earlier.

Open Eclipse preferences and go to Maven→Archetypes window, click Add Local Catalog…​ to add the file you just created.

local-catalog
Figure 22. Add local archetype-catalog.xml to eclipse

1.7.6. Developing and deploying a Site Service Extension library

Create a new project using the com.eidosmedia.portal:archetype.web-base.extensions:

web-base-extension-eclipse
Figure 23. Create Site service extensions archetype

Select appropriate values for the groupId, artifactId, and version number. Eventually you could override the suggested package if needed.

configuring-maven-project
Figure 24. Configuring the project

If it is the first time you create a Cobalt extensions project, It will take a bit to download dependencies and build the project, before it is available and ready in the Project Explorer window.

To deploy your app use the eclipse Add and Remove feature, by right-clicking the Cobalt server:

add-and-remove
Figure 25. Add module to server 1

Select your project, click Add and then Finish

add-module-to-server
Figure 26. Add module to server 2

You can now run or debug your server.

1.7.7. Developing and deploying a Directory Service Extension library

Create a new project using the com.eidosmedia.portal:archetype.directory.extensions:

directory-extension-eclipse
Figure 27. Create Directory service extensions archetype
directory-extension-eclipse02
Figure 28. Create Directory service extensions archetype

After choosing the correct archetype you must fill the fields into the dialog window. The properties allow you to choose the name of the classes that will be created:

  • resourceName: Specifies the name of the class that contains the REST endpoints.

  • connectorName: Specifies the name of the class that contains the connector’s logic.

  • connectorKey: Specifies the value of a constant used to identify the connector into the directory module.

  • dataName: Specifies the name of the class that contains the bean class.

  • resourcePath: Specifies the REST endpoint.

Deploy your app using the eclipse Add and Remove feature as explained in Developing and deploying a Site Service Extension library

1.7.8. Developing and deploying a Web Module

You can run web modules on Cobalt as you would normally do with any J2EE application server or servlet container.

By running web modules on the Cobalt Server you get access to a set of shared resources, granting single sign on, and automatic registration and discovery of modules.

Use the maven archetype to create a new Cobalt Web Module:

maven-archetype-web-module
Figure 29. Web Module Maven Archetype

This will create a new maven web-module with a pre-configured pom.

Add the module to the server with its Add and Remove feature:

add-module
Figure 30. Add module to the server

This will create a sample context xml entry in the conf/Catalina/localhost folder of the server configuration. Remove the .sample suffix and name the context as the context path you desire:

deploy-web-module
Figure 31. Deploy module to the server

You can start developing your own service using Cobalt shared resources.

2. Common Development Patterns

2.1. Logging

Cobalt uses slf4j interfaces for logging. It’s a common practice to create a static Logger instance on top of your class:

private static Logger logger = LoggerFactory.getLogger(YourClass.class);

2.2. Dependency Injection

Cobalt relies internally on dependency injection frameworks. When developing extensions, for the Site, Directory and other Cobalt services, you are able to inject core components or utility directly in your implementations of the extension interface we provide.

2.2.1. Jersey Client

You can inject a preconfigured Jersey Client instance to perform request to web services:

private Client client;

@Inject
public void setClient(Client client) {
    this.client = client;
}

Or you can customize the configuration:

@Inject
public void setClient(Client client) {
    this.client = client.property(ClientProperties.CONNECT_TIMEOUT, 3000)
            .property(ClientProperties.READ_TIMEOUT, 3000);
}

2.2.2. Jackson ObjectMapper

You can inject a preconfigured Jackson ObjectMapper to work with json:

private ObjectMapper mapper;

@Inject
public void setObjectMapper(Object mapper) {
    this.mapper = mapper;
}

2.2.3. ServiceDiscoveryClient

You can inject a com.eidosmedia.portal.servicediscovery.ServiceDiscoveryClient instance and use its methods to discovery other services:

private ServiceDiscoveryClient sdClient;

@Inject
public void setServiceDiscoveryClient(ServiceDiscoveryClient sdClient) {
    this.sdClient = sdClient;
}

To discover the url of the MODERATION service, for example:

String url = sdClient.getServiceUri(ServiceType.MODERATION);

This assumes that one or more MODERATION services are available in the same zone/domain of the service calling the method.

Zone and Domain depends on the specific deployment and how cobalt services are distributed in your environment.

To specify where to look, for the service URL lookup, you need to use:

String url = sdClient.getServiceUri(domain, zone, ServiceType.MODERATION, null);

to specify the coordinates of the requested service.

2.2.4. ServiceInfo

A com.eidosmedia.portal.ServiceInfo bean is available to get information on the running service.

private ServiceInfo info;

@Inject
public void setServiceInfo(ServiceInfo info) {
    this.info = info;
}

3. Publication Process Customization

3.1. Define new custom types

Cobalt is released with a set of predefined types. This list is visible through the REST API /core/types.

You can create your custom own types, and then reuse them in the publishing process.

Suppose you want to create a new type to publish video stories. However, being articles we want to keep article as their base type, and define the new type videostory.

To do this, edit the src/pub/custom-types.xml file by adding this line to the <CustomTypes /> block:

<Type typeName="videostory" baseType="article" label="Video story" description="This is an article with lots of videos inside" icon="videostory.icon" />

Cobalt will update the list of hot types without having to restart the server. Wait a few moments and the new type will be usable in your new publications.

If you want to create a custom base type, you have to leave empty the definition of the baseType inside the <Type /> block.

<Type typeName="recipe" baseType="" label="Recipe" description="Recipe" icon="recipe.icon" />

3.2. Publish from an external source

You can publish from you custom own repository, by using the APIs of the Publication Service (exposed by the Cobalt Core Service).

The main steps you have to do are the following:

  1. Make a login to the Directory Service to get a session token (emauth). You’ll use this token to autheticate yourself on the next calls

  2. Start a new publication to get a new publication id

  3. Upload all the resource involved in the publication (with their content and metadata)

  4. If all the the uploads succeed, commit the publication, otherwise abort/rollback it

  5. Logout from your opened session

3.2.1. Login

Send username and password, to get back the session id (emauth).

Request (/directory/sessions/login)
POST /directory/sessions/login HTTP/1.1
Host: localhost:8480
Content-Type: application/json
Cache-Control: no-cache

{
	"name": "aleber",
	"password": "al3b3r"
}
Response
{
    "user": {
        "type": "USER",
        "created": "2018-07-29T14:04:27.848Z",
        "creatorId": "32b3c7f6-072b-4ab8-adfc-27ae06d5ff52",
        "lastModifierId": "32b3c7f6-072b-4ab8-adfc-27ae06d5ff52",
        "modified": "2018-07-29T14:04:27.848Z",
        "version": "1.0",
        "name": "aleber",
        "alias": "aleber",
        "role": "USER",
        "status": "ENABLED",
        "lastLogin": "2018-07-29T14:05:26.144Z",
        "bookmarks": [],
        "id": "7c785cb3-97e9-4eac-a531-9b1902528cb9"
    },
    "session": {
        "created": "2018-07-29T14:05:26.144Z",
        "creatorId": "7c785cb3-97e9-4eac-a531-9b1902528cb9",
        "lastModifierId": "7c785cb3-97e9-4eac-a531-9b1902528cb9",
        "modified": "2018-07-29T14:05:26.144Z",
        "version": "1.0",
        "ip": "127.0.0.1",
        "lastAccess": "2018-07-29T14:05:26.144Z",
        "rememberMe": false,
        "id": "93995770-8cbe-4c76-9536-ea9cc079a461"
    }
}

The emauth is the value in session.id, in this case "93995770-8cbe-4c76-9536-ea9cc079a461".

3.2.2. Generate publication id (prepare)

Send a request to get a new publication id. In the publication you can include all the objects that are involved, this means that with a single publication you can upload complex objects, like a web page with many articles.

Request (/core/publication/prepare)
POST /core/publication/prepare?emauth=f40ca28e-b5bc-4b2c-9743-3c21a2cd0ee5 HTTP/1.1
Host: localhost:8480
Content-Type: application/json
Cache-Control: no-cache

{
	"sites": ["test-site"],
	"refs": [{
		"reference": "art0-ref",
		"source": "art0-source"
	}],
	"prepareInfo": {
		"publishSource": "my-source",
		"publishDate": "2018-07-27T12:13:14.156Z"
	}
}

reference is a unique key for your publish source. This gives you the possibility to update an already published content (using the same reference on another publication).

Response
{
    "publishId": "0247-0a4286346f49-8fe1435ff964-0003",
}

You’ll need this publication id (publishId) to do the other publication steps.

3.2.3. Upload contents (update)

For all the elements/contents involved in the publication, you need to make a separate call to the /core/publication/update endpoint.

Each call is a multipart/formdata POST containing at least two parts:

  • data: a json with all the metadata of the content (and the references to all the other files)

  • content: the binary file of the content

For example suppose we want to add a simple XML content.

Request (/core/publication/update)
POST /core/publication/update?emauth=52a3cf82-9b81-46a3-a23a-fe1250234c6a&amp;pubId=0247-0a42727e4f1a-7fb0caac81f7-0003 HTTP/1.1
Host: localhost:8480
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Cache-Control: no-cache

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="data"; filename="art0.json"
Content-Type: application/json


------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="content"; filename="art0.xml"
Content-Type: text/xml


------WebKitFormBoundary7MA4YWxkTrZu0gW--
art0.json
{
	"foreignId": "art0-ref",
	"title": "my title art0",
	"summary": "my summary",
	"authors": ["ale.ber"],
	"sys": {
		"baseType": "article",
		"type": "article"
	},
	"attributes": {
		"custom": {
			"myattr": "myval"
		},
		"tags": ["t1"]
	},
	"files": {
		"content": {
			"fileName": "art0.xml",
			"partName": "content",
			"mimeType": "text/xml",
			"data": null
		}
	},
	"pubInfoEx": {
		"test-site": {
			"siteName": "test-site",
			"sectionPath": ""
		}
	}
}
art0.xml
<?xml version="1.0" encoding="UTF-8"?>
<document>
	<headgroup>
		<headline>
			<p>My title</p>
		</headline>
	</headgroup>
	<summary>
		<p>My summary</p>
	</summary>
	<byline>
		<p>ale.ber</p>
	</byline>
	<content>
		<p>Lorem ipsum</p>
	</content>
</document>
Response

                                            

In case of success the response is an empty JSON, the status code is 200.

3.2.4. Commit the publication (publish)

Request (/core/publication/publish)
POST /core/publication/publish?emauth=f40ca28e-b5bc-4b2c-9743-3c21a2cd0ee5&amp;pubId=0247-0a42727e4f1a-7fb0caac81f7-0003 HTTP/1.1
Host: localhost:8480
Content-Type: application/json
Cache-Control: no-cache
Response

                                            

In case of success the response is an empty JSON, the status code is 200.

3.2.5. Abort the publication

Request (/core/publication/abort)
POST /core/publication/abort?emauth=f40ca28e-b5bc-4b2c-9743-3c21a2cd0ee5&amp;pubId=0247-0a42727e4f1a-7fb0caac81f7-0003 HTTP/1.1
Host: localhost:8480
Content-Type: application/json
Cache-Control: no-cache
Response

                                            

In case of success the response is an empty JSON, the status code is 200.

3.2.6. One publication with many files correlated

If you want to make a publication with many contents in it, you need to adjust a little bit what we have done in the previous steps.

Suppose we want to publish an article (art1.xml) with a main image (main.jpg) and also an image inside its body (body.jpg).

In the prepare step you have to declare all the three items.

Prepare with multiple files
POST /core/publication/prepare?emauth=d8671b29-1b62-48c1-99fb-27649460aff8 HTTP/1.1
Host: localhost:8480
Content-Type: application/json
Cache-Control: no-cache
Postman-Token: db3ae370-f30c-4d3d-a765-bdfec3e194c3

{
	"sites": ["test-site"],
	"refs": [{
		"reference": "art1-ref",
		"source": "art1-source"
	}, {
		"reference": "main-img-ref",
		"source": "main-img-source"
	}, {
		"reference": "body-img-ref",
		"source": "body-img-source"
	}],
	"prepareInfo": {
		"publishSource": "my-source",
		"publishDate": "2018-07-27T12:13:14.156Z"
	}
}

As before, the response will contains the publishId.

Then you can upload all the resources, one by one.

Update body image
POST /core/publication/update?emauth=b7562682-e096-4383-8cf9-fd1786c96bf5&amp;pubId=0247-0a42bfe06e30-5cc32edd8977-0003 HTTP/1.1
Host: localhost:8480
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Cache-Control: no-cache

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="data"; filename="main-img.json"
Content-Type: application/json


------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="content"; filename="main.jpg"
Content-Type: image/jpeg


------WebKitFormBoundary7MA4YWxkTrZu0gW--
body.img.json
{
	"foreignId": "body-img-ref",
	"title": "body image",
	"summary": "body image",
	"authors": ["ale.ber"],
	"sys": {
		"baseType": "image",
		"type": "image"
	},
	"attributes": {
		"custom": {}
	},
	"files": {
		"content": {
			"fileName": "body-img.jpg",
			"partName": "content",
			"mimeType": "image/jpeg",
			"data": null
		}
	},
	"pubInfoEx": {
		"test-site": {
			"siteName": "test-site",
			"sectionPath": ""
		}
	}
}
Update main image
POST /core/publication/update?emauth=b7562682-e096-4383-8cf9-fd1786c96bf5&amp;pubId=0247-0a42bfe06e30-5cc32edd8977-0003 HTTP/1.1
Host: localhost:8480
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Cache-Control: no-cache
Postman-Token: e2576f16-841c-455c-aea0-3b1954c35278

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="data"; filename="body-img.json"
Content-Type: application/json


------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="content"; filename="body.jpg"
Content-Type: image/jpeg


------WebKitFormBoundary7MA4YWxkTrZu0gW--
main.img.json
{
	"foreignId": "main-img-ref",
	"title": "main image",
	"summary": "main image",
	"authors": ["ale.ber"],
	"sys": {
		"baseType": "image",
		"type": "image"
	},
	"attributes": {
		"custom": {}
	},
	"files": {
		"content": {
			"fileName": "main-img.jpg",
			"partName": "content",
			"mimeType": "image/jpeg",
			"data": null
		}
	},
	"pubInfoEx": {
		"test-site": {
			"siteName": "test-site",
			"sectionPath": ""
		}
	}
}
Update article
POST /core/publication/update?emauth=b7562682-e096-4383-8cf9-fd1786c96bf5&amp;pubId=0247-0a42bfe06e30-5cc32edd8977-0003 HTTP/1.1
Host: localhost:8480
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Cache-Control: no-cache
Postman-Token: 6f02cffb-a216-42de-a782-48ca60218b52

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="data"; filename="art1.json"
Content-Type: application/json


------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="content"; filename="art1.xml"
Content-Type: text/xml


------WebKitFormBoundary7MA4YWxkTrZu0gW--
art1.json
{
	"foreignId": "art1-ref",
	"title": "my title art1",
	"summary": "my summary",
	"authors": ["ale.ber"],
	"sys": {
		"baseType": "article",
		"type": "article"
	},
	"attributes": {
		"custom": {}
	},
	"links": {
		"hyperlink": {
			"image": [{
				"foreignId": "body-img-ref"
			}]
		},
		"system": {
			"mainPicture": [{
				"foreignId": "main-img-ref"
			}]
		}
	},
	"files": {
		"content": {
			"fileName": "content.xml",
			"partName": "content",
			"mimeType": "text/xml",
			"data": null
		}
	},
	"pubInfoEx": {
		"test-site": {
			"siteName": "test-site",
			"sectionPath": ""
		}
	}
}

In the links node you can define the links between the content and the used images.

art1.xml
<?xml version="1.0" encoding="UTF-8"?>
<document>
	<headgroup>
		<headline>
			<p>My title</p>
		</headline>
	</headgroup>
	<mediagroup>
		<figure>
			<img src="cobaltextref:main-img-ref?format=content" />
		</figure>
	</mediagroup>
	<summary>
		<p>My summary</p>
	</summary>
	<byline>
		<p>ale.ber</p>
	</byline>
	<content>
		<p>
			Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce tempor malesuada elit, dapibus eleifend dolor ultricies non. Donec malesuada nisl lorem, id luctus ipsum hendrerit vitae. Nam efficitur ex sed nibh blandit, eget sagittis mauris euismod. Etiam vitae congue metus, non pharetra sem.
		</p>
		<figure>
			<img src="cobaltextref:body-img-ref?format=content" />
		</figure>
		<p>
			Etiam eros purus, porttitor et placerat quis, auctor in dui. Sed enim erat, lobortis vitae posuere tempor, feugiat laoreet lorem. Etiam a sollicitudin turpis. Maecenas sit amet tincidunt nibh. Nulla facilisi. Nullam diam odio, aliquam at ultrices eu, congue sed quam. Curabitur id pulvinar nisi, id egestas ligula. Mauris luctus feugiat enim eget ultrices.
		</p>
	</content>
</document>

In the XML content, you can refer to the linked images with their external references. Cobalt will convert them into Cobalt ids during the publication.

3.3. Hooks for publication events

Cobalt has a dedicated service to notify publication events: PSN - Publication Service Notifier.

The events that are notified are of three types:

  • created content

  • modified content

  • deleted content

Cobalt also comes with two example implementations: one will write the notification information into the logger, the other will send the noditification information to the configured endpoint as an HTTP POST call.

These are very trivial and probably useless implementations in a real case, but you can use them as a starting point to create your implementations.

PSN can be configured from its conf/cobalt/psn.xml file. In this file you can define all the subscribers you need. For each subscriber you can define:

  • a list of endpoints

  • a comma separated list of the sites you are interested in (or * for all sites)

  • a comma separated list of the types you are interested in (or * for all the types)

For example:

<Subscribers>
    <Subscriber id="test" sites="*" types="article">
        <Endpoint em-type="com.eidosmedia.portal.psn.endpoint.ConsoleEndpoint" />
        <Endpoint url="http://www.example.com/my/service">
            <Header name="X-Powered-By"
                values="Cobalt Publication Service Notifier" />
        </Endpoint>
    </Subscriber>
</Subscribers>

3.3.1. HTTP POSTs Notifications

To add a new HTTP POST endpoint, you have to add an <Endpoint /> to a <Subscriber />.

For example:

<Subscriber id="test" sites="*" types="article">
    <Endpoint url="http://www.example.com/my/service">
        <Header name="X-Powered-By"
            values="Cobalt Publication Service Notifier" />
    </Endpoint>
</Subscriber>

This will make an HTTP POST call everytime an article will be published, updated or unpublished (for all the sites handled by Cobalt). You can add custom headers by configuration, as shown on the previous example.

The body of the request will contains the notification data in a JSON format. For example, when you publish an article you’ll get a body like the following:

{
  "siteName" : "test-site",
  "created" : [ {
    "publishDate" : "2018-07-27T07:06:46.318Z",
    "parentNodeId" : "4000-0a3603750a8b-042974932c13-2000",
    "nodeId" : "0247-0a41cee84fe3-b3333a23fd9f-1000",
    "title" : "This is a test article",
    "summary" : "This article is used to test PSN",
    "type" : "article",
    "path" : "/",
    "visible" : true,
    "url" : "http://www.site.test:8480/0247-0a41cee84fe3-b3333a23fd9f-1000/index.html"
  } ],
  "updated" : [ ],
  "deleted" : [ ]
}

If you update the same article, you get a notification like the following:

{
  "siteName" : "test-site",
  "created" : [ ],
  "updated" : [ {
    "publishDate" : "2018-07-27T07:08:41.563Z",
    "parentNodeId" : "4000-0a3603750a8b-042974932c13-2000",
    "nodeId" : "0247-0a41cee84fe3-b3333a23fd9f-1000",
    "title" : "This is a test article, updated",
    "summary" : "This article is used to test PSN",
    "type" : "article",
    "path" : "/",
    "visible" : true,
    "url" : "http://www.site.test:8480/0247-0a41cee84fe3-b3333a23fd9f-1000/index.html"
  } ],
  "deleted" : [ ]
}

Finally, if you unpublish this article from Cobalt (you can keep it on the editorial side), you’ll get a notification like the following:

{
  "siteName" : "test-site",
  "created" : [ ],
  "updated" : [ ],
  "deleted" : [ {
    "publishDate" : "2018-07-27T07:10:11.186Z",
    "parentNodeId" : "4000-0a3603750a8b-042974932c13-2000",
    "nodeId" : "0247-0a41cee84fe3-b3333a23fd9f-1000",
    "title" : "This is a test article, updated",
    "summary" : "This article is used to test PSN",
    "type" : "article",
    "path" : "/",
    "visible" : true
  } ]
}

Obviously, in the three arrays (created, updated and deleted) you have only one items because I’m publishing an article with an image, and I declare an Subscriber for only article.

If I change my subscriber settings to handle all types, I would have 2 items in the created array (one article and one image). Something like that:

{
  "siteName" : "test-site",
  "created" : [ {
    "publishDate" : "2018-07-27T07:17:50.597Z",
    "parentNodeId" : "4000-0a3603750a8b-042974932c13-2000",
    "nodeId" : "0247-0a41d2ded0dc-e84b446fd375-1000",
    "title" : "This is an image used for PSN test",
    "summary" : "",
    "type" : "image",
    "path" : "/",
    "visible" : true,
    "url" : "http://www.site.test:8480/0247-0a41d2ded0dc-e84b446fd375-1000/index.html"
  }, {
    "publishDate" : "2018-07-27T07:17:50.597Z",
    "parentNodeId" : "4000-0a3603750a8b-042974932c13-2000",
    "nodeId" : "0247-0a41d2ded0df-bc264807a924-1000",
    "title" : "This is a test article with an image",
    "summary" : "This article is used to test PSN",
    "type" : "article",
    "path" : "/",
    "visible" : true,
    "url" : "http://www.site.test:8480/0247-0a41d2ded0df-bc264807a924-1000/index.html"
  } ],
  "updated" : [ ],
  "deleted" : [ ]
}

3.3.2. Custom Java Notifications

You can create you custom own Java handler to do whatever you want with the notification data event.

To do that you can simply create a new Maven project by using the provided archetype (com.eidosmedia.portal:archetype.psn.extensions).

This will create a project with an example class that show you how you can handle the notification event. It simply right all the data on the configured logger.

package com.example.mypsnext;

import java.text.SimpleDateFormat;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.eidosmedia.portal.psn.ExtendedPublishNodeNotification;
import com.eidosmedia.portal.psn.NotificationData;
import com.eidosmedia.portal.psn.endpoint.Endpoint;

public class MyEndpoint implements Endpoint {
    private static final Logger logger = LoggerFactory.getLogger(MyEndpoint.class);

    private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz");

    @Override
    public void sendNotificationData(NotificationData notificationData) {
        // created contents
        List<ExtendedPublishNodeNotification> created = notificationData.getCreated();
        if (created != null) {
            for (ExtendedPublishNodeNotification epnn : created) {
                logger.debug("A new {} has been CREATED for site {}!", epnn.getType(), notificationData.getSiteName());
                logger.debug("    id:      {}", epnn.getNodeId());
                logger.debug("    url:     {}", epnn.getUrl());
                logger.debug("    title:   {}", epnn.getTitle());
                logger.debug("    summary: {}", epnn.getSummary());
                logger.debug("    pubDate: {}", SDF.format(epnn.getPublishDate()));
            }
        }
        // updated contents
        List<ExtendedPublishNodeNotification> updated = notificationData.getUpdated();
        if (updated != null) {
            for (ExtendedPublishNodeNotification epnn : updated) {
                logger.debug("An existing {} has been UPDATED for site {}!", epnn.getType(),
                             notificationData.getSiteName());
                logger.debug("    id:      {}", epnn.getNodeId());
                logger.debug("    url:     {}", epnn.getUrl());
                logger.debug("    title:   {}", epnn.getTitle());
                logger.debug("    summary: {}", epnn.getSummary());
                logger.debug("    pubDate: {}", SDF.format(epnn.getPublishDate()));
            }
        }
        // deleted contents
        List<ExtendedPublishNodeNotification> deleted = notificationData.getDeleted();
        if (deleted != null) {
            for (ExtendedPublishNodeNotification epnn : deleted) {
                logger.debug("An existing {} has been DELETED for site {}!", epnn.getType(),
                             notificationData.getSiteName());
                logger.debug("    id:      {}", epnn.getNodeId());
                logger.debug("    title:   {}", epnn.getTitle());
                logger.debug("    summary: {}", epnn.getSummary());
                logger.debug("    pubDate: {}", SDF.format(epnn.getPublishDate()));
            }
        }
    }

}

As you can see, creating a new notification handler means to implement a very simple interface (com.eidosmedia.portal.psn.endpoint.Endpoint) with only one method (sendNotificationData(NotificationData)).

If needed, in those type of extensions you can inject the repository service and the discovery service.

Once you have built your jar file, you can deploy it under extra/lib/psn folder. This is not enough, because you have to add this new Endpoint to the PSN configuration file (conf/cobalt/psn.xml) as shown before. For example:

<Subscriber id="test" sites="*" types="article">
    <Endpoint em-typ="com.example.psn.subscribers.custom" />
</Subscriber>

If your custom endpoint need to be configured from the XML file, you can implements the com.eidosmedia.portal.Configurable interface with one single methos (i.e. com.eidosmedia.portal.psn.endpoint.HttpEndpoint.loadConfiguration(HierarchicalConfiguration)). For example:

public class MyCustom implements Endpoint, Configurable {

    private String something;

    @Override
    public void loadConfiguration(HierarchicalConfiguration configuration)
        throws ConfigurationException {
        this.something = configuration.getString("[@something]");
    }

    // ...
}

4. Themes

Cobalt is released with a single theme, called default, which serves only as a reference base for the development of custom themes and as a fallback theme in case a particular resource is not defined within the custom theme. It is not directly visible because it’s contained within the site module. In the cobalt distribution package we are releasing a copy of the default theme, called sample (it’s inside the src/themes folder). We also release a zip file containing an empty theme that can be used as a starting point for new themes development (the file is placed in src/themes/blank_theme.zip).

For this simple walkthrough, we’re going to use a freely availble Bootstrap template, downloadable from Start Bootstrap - Clean Blog. It’s a very basic theme (essentially oriented for a blog site), but it will be sufficient to show how to create a new Cobalt theme.

For semplicity, we suppose to create the theme on a developer PC, using Eclipse as IDE with the Cobalt SDK installed. Obviously you can do that from a different IDE, but in this case you have to manually copy your files in the correct folder of the Cobalt instance (your-cobalt-base-folder/src/themes/your-new-theme).

We also suppose to have already created a site named mysite which responds at http://www.mysite.test:8480.

4.1. Theme folder preparation

As mentioned before, we can bootstrap our new theme by using the blank_theme.zip provided within the Cobalt released package. You can find this zip file within the Servers > Cobalt Server 1.0 at localhost-config > src > themes folder.

theme-server-folder-structure
Figure 32. Cobalt Server folder structure in Eclipse with Cobalt SDK

Unzip this file directly within the themes folder and call that folder mytheme.

theme-server-folder-structure
Figure 33. mytheme folder inside the themes folder

Downnload the Start Bootstrap - Clean Blog template and extract its content inside the mytheme/assets folder.

theme-template-folder-structure
Figure 34. Start Bootstrap - Clean Blog template extracted inside the theme assets folder

Since you are developing a new theme, you don’t want to modify the real settings of your site. Anyhow you can tell to your local instance of the Site module to use this new theme when serving your requested site pages. To do that you have to place a file called site.properties inside your src/www/mysite folder (remember that mysite is the name of an existing site). In this file, place a property theme valued as mytheme (the name of your just created custom theme).

theme=mytheme

Please note that this theme overriding process is available only when you are in development mode, namely you have the proerty development.mode=true in your cobalt.properties file.

If at this point you’ll try to load the site from your browser, you’ll see a very bad result (the default theme mixed with the current theme’s assets css files). Don’t worry, it’s all under control.

4.2. Section page

We consider a section page as a simple list of articles, sorted by chronological order. Later we’ll see how it’s possible to model the contents of a section page, not as a flat list of articles, but as a sequence of lists of objects.

In the section’s model you have an array called children containing a sorted list of ids.

These ids can be used to retrieve the relative node info directly from within the section model in the nodes map.

The first thing you have to do is to identify the resource inside your asset folder that is going to represent this kind of page. The resource is the assets/index.html file.

Copy this file in the templates folder and call it section.ftl.

If at this point you’ll try to open your site root page, namely http://www.mysite.test:8480/, you’ll see the original index.html page rendered.

theme-clean-blog
Figure 35. The static home page served

Please note that the root of a site doesn’t have section type, instead it has site type. Anyhow, in this case the site root is rendered by using the section.ftl file (and not the site.ftl file) simply because in the default theme we have put this in the site.ftl template.

<@include template="section.ftl"/>

To change the title and the description of the page, you can substitute this HTML code in the section.ftl template:

    <!-- Page Header -->
    <header class="masthead" style="background-image: url('img/home-bg.jpg')">
      <div class="overlay"></div>
      <div class="container">
        <div class="row">
          <div class="col-lg-8 col-md-10 mx-auto">
            <div class="site-heading">
              <h1>Clean Blog</h1>
              <span class="subheading">A Blog Theme by Start Bootstrap</span>
            </div>
          </div>
        </div>
      </div>
    </header>

with this:

    <!-- Page Header -->
    <header class="masthead" style="background-image: url('img/home-bg.jpg')">
      <div class="overlay"></div>
      <div class="container">
        <div class="row">
          <div class="col-lg-8 col-md-10 mx-auto">
            <div class="site-heading">
              <h1>${currentObject.title}</h1>
              <span class="subheading">${currentObject.description}</span>
            </div>
          </div>
        </div>
      </div>
    </header>

We simply substitute the title and description text with the currentObject title and description value, provided by the page model.

The HTML block that represent a single article is this one.

          <div class="post-preview">
            <a href="post.html">
              <h2 class="post-title">
                Man must explore, and this is exploration at its greatest
              </h2>
              <h3 class="post-subtitle">
                Problems look mighty small from 150 miles up
              </h3>
            </a>
            <p class="post-meta">Posted by
              <a href="#">Start Bootstrap</a>
              on September 24, 2018</p>
          </div>
          <hr>

We can keep just one of this block and remove all the others. Then we can list all the children ids present in the model, and populate a block for each of them.

<#list model.children as childId>
        <#assign child=model.nodes[childId]>
          <div class="post-preview">
            <a href="${url(child)}">
              <h2 class="post-title">
                ${child.title!}
              </h2>
              <h3 class="post-subtitle">
                ${child.summary!}
              </h3>
            </a>
            <p class="post-meta">Posted by
              <a href="#">${child.authors[0]}</a>
              on ${child.pubInfo.publicationTime?date}</p>
          </div>
          <hr>
</#list>

The most interesting things here are the way we navigate the nodes list (through <#list> Freemarker directive), and the way we calculate the article url (by using a Freemarker method exposed by Cobalt, ${url(child)}).

theme-section
Figure 36. The home page with dynamic content rendered

4.3. Article page

The article page in our base template is represented by the assets/post.html file. As we have done for the section page, we have to copy this post.html file inside the templates folder and rename it as article.ftl.

Once we have done that, we can proceed to put Freemarker directive within the HTML code.

For the page header we can write the following code:

    <!-- Page Header -->
    <#assign bgimg=resourceUrl(currentObject.picture).default(theme.assetUrl('img/post-bg.jpg'))>
    <header class="masthead"
            style="background-image: url('${bgimg}')">
      <div class="overlay"></div>
      <div class="container">
        <div class="row">
          <div class="col-lg-8 col-md-10 mx-auto">
            <div class="post-heading">
              <h1>${currentObject.title!}</h1>
              <h2 class="subheading">${currentObject.summary!}</h2>
              <span class="meta">Posted by
                <a href="#">${currentObject.authors[0]!}</a>
                on ${currentObject.pubInfo.publicationTime?date}</span>
            </div>
          </div>
        </div>
      </div>
    </header>

Please note that if at this point you’ll try to open the article page, you’ll get a very bad result, because the url of all the static resources (CSS and JS references) are wrongly interpreted. This because the references within the HTML are relative and the article url introduce some slashes. To fix this you have to modify all the resource imports, by placing a / in front of all the href/scr attributes. For example, this line:

    <script src="vendor/jquery/jquery.min.js"></script>

have to be fix in this way:

    <script src="/vendor/jquery/jquery.min.js"></script>

Now it’s time to render the content.

The content is contained within the currentObject.files.content.data of your underlying model. Since we are rendering the content of an article and its content is in HTML format, we could make the direct output of this field.

    <!-- Post Content -->
    <article>
      <div class="container">
        <div class="row">
          <div class="col-lg-8 col-md-10 mx-auto">
            ${currentObject.files.content.data}
          </div>
        </div>
      </div>
    </article>

Anyhow, you will probably need to transform this content, for example to clean up some useless blocks or to transform standard blocks into HTML code that conforms to your theme. To do this you can use the transform directive, which will apply an XSL transformation to the selected node.

As stated in the documentation, the article’s content is available also through currentObject.xmlContent property, which expose the XML parsed content of the article. By using this, you can easily select the node inside your content and pass it to the render function.

Our current article content is this one:

<document emk-type="story">
  <headgroup emk-id="RKX8cctJFwCjZ2e8g48N17O">
    <headline emk-id="">
      <p>My first article</p>
    </headline>
  </headgroup>
  <mediagroup>
    <figure emk-channel="Globe-Web,Cobalt-Blog1,AbTest,Globe-Print,Globe-Tablet,Globe-Mobile" emk-type="photo-normal">
      <img class="normal xsm-blockstyle-align-center" data-id="0247-0a203ee1f5e6-962a33938ae7-1000" emk-id="U85815164040xiY" height="450" src="/resources/0247-0a203ee1f5e6-962a33938ae7-1000/....jpeg" width="800">
    </figure>
  </mediagroup>
  <linkgroup></linkgroup>
  <summary emk-id="RsysEoqUKF4gqRm9KCXzDrJ">
    <p>This is my first article</p>
  </summary>
  <content emk-id="RL1YZJyW22zn2bQD26U4W3N">
    <byline>Ale Ber</byline>
    <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
  </content>
  <style></style>
</document>

Since we only need to output the document.content of the article we can pass only this xpath to the transform directive.

    <!-- Post Content -->
    <article>
      <div class="container">
        <div class="row">
          <div class="col-lg-8 col-md-10 mx-auto">
            <@transform content=currentObject.xmlContent.document.content />
          </div>
        </div>
      </div>
    </article>

If in the transform directive you do not specify an XSL transformation file (throught the xsl attribute), the src/themes/mytheme/default.xsl file will be used. If this file does not exist, the default theme default.xsl file will be used.

Since we don’t want to ouptut the byline content and we don’t need to have a content block around the outputted content, we can create a simple XSL as follow.

<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xalan="http://xml.apache.org/xalan"
    xmlns:cobalt="xalan://com.eidosmedia.portal.impl.xsl.CobaltElement"
    extension-element-prefixes="cobalt" exclude-result-prefixes="xalan">

    <xsl:output method="html" indent="yes"
        omit-xml-declaration="yes" xalan:indent-amount="4" />

    <xsl:template match="content">
        <xsl:apply-templates select="p" />
    </xsl:template>

</xsl:stylesheet>

Doing this, the transform directive will simply output that HTML fragment.

<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
theme-article-page
Figure 37. The rendered article page

4.4. Webpage page

A webpage can be seen as a container. This container is organized in different zones. A zone is itself a container and it could be organized in different sequences. A sequence is another container that can contains different elements.

webpage WP1
|- zone Z1
|  |- sequence S1
|  |  |- content C1
|  |  |- content C2
|  |  |- content CX
|  |- sequence S2
|  |  |- content C3
|  |  |- content CY
|- zone Z2
|  |- sequence S3
|  |  |- content C4

This structure can be reconstructed from the model of the page, mainly by using the defined links.

Anyhow, we developed Freemarker extensions that help you to navigate the webpage hierarchy.

<ul>
  <#list zones()?keys as zoneName>
    <#assign azone=zone(zoneName)>
    <li>
      zone ${zoneName}
      <ul>
        <#list azone.sequences as sequence>
          <li>
            sequence ${sequence?counter}
            <ul>
              <#list sequence.items as item>
                <li>${item.object.title}</li>
              </#list>
            </ul>
          </li>
        </#list>
      </ul>
    </li>
  </#list>
</ul>

You can get all the zones using the zones() method. This will return a map of the zones, where the key is the zone name. To navigate this map you can simply iterate its keys set (by using zones()?keys). Once you get the zone name, you can fetch the zone by using the zone method.

You can list all the zone sequences using a simple list directive. Finally you can iterate the sequence items.

Usually you’ll have zones with a single sequence. For this reason we give you the possibility to list items directly from their zone, without the need to iterate sequences.

<ul>
  <#list zones()?keys as zoneName>
    <#assign azone=zone(zoneName)>
    <li>
      zone ${zoneName}
      <ul>
        <#list azone.items as item>
          <li>${item.object.title}</li>
        </#list>
      </ul>
    </li>
  </#list>
</ul>
remember to don’t do that: <#assign zone=zone(zoneName)> because zone is also the method name. In the code snippet we used azone instead.

Moreover, you don’t want to put some complex logic to render an item directly within the section template (in the previous example we simply output the item title).

To avoid this you can use the include directive.

<ul>
  <#list zones()?keys as zoneName>
    <#assign azone=zone(zoneName)>
    <li>
      zone ${zoneName}
      <ul>
        <#list azone.items as item>
          <li>
            <@include object=item />
          </li>
        </#list>
      </ul>
    </li>
  </#list>
</ul>

Suppose you are going to include an article, then the template engine will try to use the src/mytheme/fragments/article.ftl fragment template. You can create this file, and inside it you can treat the included item as the new currentObject.

<a href="${url(currentObject)}">${currentObject.title}</a>
theme-final-folder
Figure 38. The final theme folder

5. Site Service Extensions

Cobalt has several extensions points for the Site Service. In the following sections, we provide extensive step by step examples for each.

5.1. Injectable Components

In the extensions point, many beans and managers of the Site Service can be injected via the standard JSR-330 annotation.

5.1.1. PortalRequest

It represents the request during the processing flow in the Site Service.

The class qualified name, including package, is:

com.eidosmedia.portal.dao.PortalRequest

It is filled with information during the request processing, such as the logged in user (accessible via the getUserData method), the identifiers of the page is going to be built (getContentDescriptor method).

5.1.2. UrlResolver

It allows to:

  • eval dynamic page or static resource file (e.g. images) URLs from references of objects in the Cobalt repository.

  • resolve objects reference from dynamic page or static resource file URLs

The class qualified name, including package, is:

com.eidosmedia.portal.url.UrlResolver

It contains several eval methods, depending on the parameters you use to eval the url, such as:

  • the desired view,

  • the desired page,

  • the UrlIntent, that can be HOST_RELATIVE, or ABSOLUTE (including the hostname and protocol parts)

  • the ResolutionType, that can be CONTENT to generate url of a dynamic page, or RESOURCE to generate url of a static resource

The eval methods require as input either the NodeId or the NodeData objects referencing an object in the Cobalt repository.

The resolveResourceUrl and resolveUrl resolve respectively the FileId of a specific static resource or an UrlResolverMatch. This latter class reference either a page, via its ContentDescriptor identifier, or redirection coordinates (an URL to redirect to and a status code).

public class UrlResolverMatch {

    private final ContentDescriptor contentDescriptor;

    private final int statusCode;

    private final URI urlTo;

    public ContentDescriptor getContentDescriptor() {
        return contentDescriptor;
    }

    public int getStatusCode() {
        return statusCode;
    }

    public URI getUrlTo() {
        return urlTo;
    }

    public UrlResolverMatch(ContentDescriptor contentDescriptor) {
        this.contentDescriptor = contentDescriptor;
        this.statusCode = 0;
        this.urlTo = null;
    }

    public UrlResolverMatch(int statusCode, URI urlTo) {
        this.contentDescriptor = null;
        this.statusCode = statusCode;
        this.urlTo = urlTo;
    }

}

5.1.3. DataManager

It is the main access point for direct access objects in the Cobalt repository and full-text queries.

The class qualified name, including package, is:

com.eidosmedia.portal.dao.DataManager
Retrieving nodes by Id
NodeData getNodeData(SiteKey siteKey, NodeId nodeId, String[] aggregators, PortalRequest portalRequest) throws DataException;
DataResult<NodeData> getNodesData(SiteKey siteKey, List<NodeId> nodeIds, String[] aggregators, PortalRequest portalRequest) throws DataException;

where the aggregators parameter can be used to provide an array of string constants among the ones available in the class:

com.eidosmedia.portal.dao.Aggregators
Querying nodes in a specific section
PaginatedResult<NodeId> getNodeChildren(SiteKey siteKey, NodeData nodeData, Params params, PortalRequest portalRequest) throws DataException;
PaginatedResult<NodeId> getNodeChildren(SiteKey siteKey, String sectionPath, Params params, PortalRequest portalRequest) throws DataException;

where nodeData,or sectionPath, are the section of which we want to fetch children nodes and params is an object of type com.eidosmedia.portal.dao.Params, that allows to define filtering options.

Querying by foreign source id

You can convert a reference of the foreign system (the system you published the content from, e.g. the Methode editorial) to a reference on cobalt.

NodeId getNodeIdByForeignId(SiteKey siteKey, String foreignId, PortalRequest portalRequest) throws DataException

5.1.4. ContentDataManager

It allows to retrieve an aggregation of nodes. This aggregation is part of the model used by the template engine and it is cached in a persistent way.

The class qualified name, including package, is:

com.eidosmedia.portal.content.ContentDataManager

The main method is:

ContentData buildContentData(SiteKey siteKey, AggregationKey aggregationKey, PortalRequest portalRequest) throws Exception;

The aggregation key is the identifier for an aggregated object. Its id field is the EntityId ( usually its subclass NodeId for database NodeData objects ) of the main data node, the one driving the aggregation:

public AggregationKey(EntityId id,
                        OutputMode outputMode,
                        String[] aggregators,
                        int pageNumber,
                        String permissionVariant)

5.1.5. SitesManager

It retrieves information about sites, such as site structure, sections, permalinks.

The class qualified name, including package, is:

com.eidosmedia.portal.site.SitesManager

Many of its methods return com.eidosmedia.portal.site.SiteNode objects.

Contains utilities to retrieve:

  • the root of a website given the site identifier

  • a site map given the site identifier

  • hostname of a website given the site identifier

  • the permalink template set on a website

5.2. SiteRequestHandler

The SiteRequestHandler interface allows you to define virtual pages responding to specific paths of your websites.

Virtual pages can fetch data from external sources and are cached with the standard cobalt caching mechanisms.

public interface SiteRequestHandler {

    String getId();

    boolean match(AggregationKey aggregationKey);

    UrlResolverMatch resolveUrl(SiteKey siteKey,
                                String relativePath,
                                Map<String, String[]> queryParams,
                                PortalRequest portalRequest) throws DataException;

    default List<String> evalUrls(SiteKey siteKey,
                                  CobaltId id,
                                  PortalRequest portalRequest) {
        return null;
    }

    AbstractData evalRootEntity(SiteKey siteKey,
                                AggregationKey aggregationKey,
                                PortalRequest portalRequest) throws DataException;

    ContentData enrichContentData(SiteKey siteKey,
                                  ContentData contentData,
                                  AggregationKey aggregationKey,
                                  PortalRequest portalRequest) throws DataException;

}

5.2.1. Example: Videos Fragment

Videos fragment is an example of SiteRequestHandler that fetches a video playlist from an external provider and produces a ContentData with a custom data model.

resolveUrl method

The resolveUrl method is used to check if a website relative path matches this handler:

package com.eidosmedia.cobalt.extensions;

// imports here

public static String PREFIX = "/_ssi/fragments/videos";

@Override
public UrlResolverMatch resolveUrl(SiteKey siteKey, String relativePath, Map<String, String[]> params, PortalRequest portalRequest) throws DataException {
    // We define the rules for the URI to match our content
    if (relativePath.startsWith(PREFIX)) {
        int page = 0;
        if (relativePath.startsWith(PREFIX + "/")) {
            String suffix = relativePath.substring(PREFIX.length() + 1);
            try {
                page = Integer.parseInt(suffix);
            } catch (NumberFormatException ex) {
                LOGGER.error("Not a number suffix: {}", suffix);
                return null;
            }
        }

        // We create a new id for our data "videos"
        SiteRequestId id = new SiteRequestId("videos");

        // We create a new ContentDescriptor with outputMode FRAGMENT
        ContentDescriptor contentDescriptor = new ContentDescriptor(id,
                                                                    OutputMode.FRAGMENT,
                                                                    null,
                                                                    page,
                                                                    null);

        // We return the UrlResolverMatch with embedded content descriptor
        UrlResolverMatch m = new UrlResolverMatch(contentDescriptor);
        return m;
    }
    return null;
}

The method returns a valid UrlResolverMatch if the request url starts with the PREFIX constant, otherwise returns null.

The UrlResolverMatch is the same bean used by the Cobalt’s UrlResolver. It must return either a valid ContentDescriptor or redirect URL and status code information.

The ContentDescriptor must refer to a virtual id of type SiteRequestId. The developers must verify that unique ids are used.

public ContentDescriptor(EntityId id, OutputMode outputMode, String view, int pageNumber, String permissionVariant)

The VideosFragment produces a ContentDescriptor with OutputMode.FRAGMENT. This virtual content is supposed to be rendered as a fragment included in a page. The pageNumber is used to split the content in separate parts (identified by the ContentDescriptor), each one containing only a subset of the videos.

match method

Once the handler matches a request, its processing is delegated to a different component using the AggregationKey as identifier (containing a superset of the fields of the ContentDescriptor). For this reason a subsequent check is performed through the match method:

@Override
public boolean match(AggregationKey aggregationKey) {
    return aggregationKey.getId().equals(new SiteRequestId("videos"));
}
The implementation of the match interface method is always similar to the one above, a simple check on the id field.
evalRootEntity method

The evalRootEntity method builds the root data object that must extend the AbstractData class. Usually the CustomData concrete class is used:

@Override
public AbstractData evalRootEntity(SiteKey siteKey, AggregationKey aggregationKey, PortalRequest portalRequest) throws DataException {
    // We create a new data object with the SiteRequestId created before as ID
    CustomData d = new CustomData(aggregationKey.getId(), new Type("videos"));

    // We set 60 seconds of time to leave. This way both the ContentData object built on top of
    // this CustomData, will last for 60 seconds in cache.
    d.setCacheTTLSeconds(60);

    // We retrieve our videos from dailymotion and put them in the data object
    try {
        Videos videos = client.target("https://api.dailymotion.com/playlist")
                .path("x4dmd3")
                .path("videos")
                .queryParam("fields", "id,title,thumbnail_240_url,embed_url,embed_html")
                .queryParam("page", aggregationKey.getPageNumber() + 1).request().get(Videos.class);

        d.put("videos", videos);
    } catch (Exception ex) {
        logger.error("evalRootEntity - error contacting the external service", ex);
                throw new DataException("Unable to build the content", ex);
    }

    return d;
}

A new Type is provided as parameter to constructor of the CustomData. This type will drive the template fetched to dress the content.

Furthermore it implements the two interfaces CacheConfigurable and PortalResource:

public interface CacheConfigurable {

    Long getCacheTTLSeconds();
}

public interface PortalResource  {

    CobaltId getId();

    long getLastModified();

    String getEtag();

    long getContentLength();

    String getContentType();

    String getCharacterEncoding();

    Locale getLocale();

    String getDescription();

}

The method getCacheTTLSeconds determines the amount of time, in seconds, the ContentData, built on top of the root object (in our case, the CustomData), stays in cache.

The getEtag and getLastModified methods build, respectively, the etag and last modified header for the browser, when the Client Side Cache is enabled in the Site Service. This way the server can avoid resending data to the client when it is not necessary.

In CustomData, the getEtag and getLastModified method implementations return, respectively, a version string and a timestamp that can be set with the constructor:

public CustomData(CobaltId id, Type type, String version, Date timestamp)

The CustomData class implements the Map interface and is backed by an internal map field. The data stored in this map is visible in the json api responses as well as in the model for the template engine.

We put the result of an API call to an external video provider in the videos field of the map (See Jersey Client for details on how to inject the client to perform web service requests). These Videos and Video POJOs are used to map the response:

public static final class Videos implements Serializable {
    private static final long serialVersionUID = 1L;

    public List<Video> list;
    public Boolean has_more;
    public Integer page;

    public Boolean getHas_more() {
        return has_more;
    }

    public List<Video> getList() {
        return list;
    }

    public Integer getPage() {
        return page;
    }
}

public static final class Video implements Serializable {
        private static final long serialVersionUID = 1L;

    public String id;
    public String title;
    public String thumbnail_240_url;
    public String embed_url;
    public String embed_html;

    public String getId() {
        return id;
    }

    public String getTitle() {
        return title;
    }

    public String getThumbnail_240_url() {
        return thumbnail_240_url;
    }

    public String getEmbed_url() {
        return embed_url;
    }

    public String getEmbed_html() {
        return embed_html;
    }
}
All data must subclass AbstractData and therefore it is mandatory to implement Serializable. If using CustomData, any object put in the map must itself implement Serializable.

Be sure to throw a new DataException if the evalRootEntity code does not build a valid AbstractData object. Otherwise, whether the data object you return is valid or not, it could be put in cache.

You could also extend its abstract subclass DataUnavailableException (or use one of its already existing subclasses, e.g. DataNotFoundException) to set the specific HTTP status code for the error.

public abstract class DataUnavailableException extends DataException {
    private static final long serialVersionUID = 1L;

    private final int statusCode;

    public DataUnavailableException(int statusCode) {
        super();
        this.statusCode = statusCode;
    }

    public DataUnavailableException(String message, int statusCode, Throwable cause) {
        super(message, cause);
        this.statusCode = statusCode;
    }

    public DataUnavailableException(String message, int statusCode) {
        super(message);
        this.statusCode =  statusCode;
    }

    public DataUnavailableException(Throwable cause, int statusCode) {
        super(cause);
        this.statusCode = statusCode;
    }

    public int getStatusCode() {
        return statusCode;
    }
}
enrichContentData method

The enrichContentData method is used to aggregate additional NodeData or AbstractData instances in the nodes map. In this example we don’t need it:

@Override
public ContentData enrichContentData(SiteKey siteKey, ContentData contentData, AggregationKey aggregationKey, PortalRequest portalRequest)
        throws DataException {
    return contentData;
}
Remember to return the contentData, even if not modified.
Registering the SiteRequestHandler

With the SiteRequestHandler implemented, we need to register it in the cobalt.properties configuration file:

site.requestHandler[0].em-type=com.eidosmedia.cobalt.extensions.VideosFragment

You need to restart the Cobalt server for the change to take effect.

You could also parametrize your SiteRequestHandler configuration. For example, suppose you want to add a dynamic path parameter instead of using the PREFIX constant:

site.requestHandler[0].em-type=com.eidosmedia.cobalt.extensions.VideosFragment
site.requestHandler[0].path=/my-path

In order to read this parameter from the Java code, you need to create a setter method, and a corresponding field, in your class:

private String path;

public void setPath(String path) {
    this.path = path;
}

Be sure that the name after the set prefix of the setter method, is the same as the one in the XML configuration, but capitalized:

  • path as the property in the cobalt.properties file,

  • setPath as the java setter method.

The same configuration can be provided to the docker container as environment variable. For example in docker-compose syntax:

environment:
    - "site.requestHandler[0].em-type=com.eidosmedia.cobalt.extensions.VideosFragment"
JSON Response example

Once the SiteRequestHandler is deployed, it is reachable at /_ssi/fragments/videos, however that endpoint will now return:

no-template
Figure 39. No template found

because no template for the requested type can be found.

If you provide the debug parameter /_ssi/fragments/videos?emk.outputMode=RAW (works only with development.mode=true in the cobalt.properties configuration file, or if your host is in the trusted network e.g. common.defaults.trusted.hosts=127\\.\\d+\\.\\d+\\.\\d+|::1|0:0:0:0:0:0:0:1), you can inspect the data model:

Debug Response
{
  "model" : {
    "id" : "model://req:videos/RAW/1/null",
    "data" : {
      "dataType" : "custom",
      "videos" : {
        "list" : [ {
          "id" : "x6d3wq8",
          "title" : "Liverpool vs Chelsea 1-0 - UCL 2004/2005 - Goal & Full Highlights",
          "thumbnail_240_url" : "http://s1.dmcdn.net/o6hRW/x240-Ge8.jpg",
          "embed_url" : "http://www.dailymotion.com/embed/video/x6d3wq8",
          "embed_html" : "<iframe frameborder=\"0\" width=\"480\" height=\"270\" src=\"//www.dailymotion.com/embed/video/x6d3wq8\" allowfullscreen allow=\"autoplay\"></iframe>"
        }, {
          "id" : "x6d5uua",
          "title" : "AC Milan vs Liverpool 2-1 - UCL Final 2007 - HD 1080i Full Highlights",
          "thumbnail_240_url" : "http://s1.dmcdn.net/o79Iv/x240-6nI.jpg",
          "embed_url" : "http://www.dailymotion.com/embed/video/x6d5uua",
          "embed_html" : "<iframe frameborder=\"0\" width=\"480\" height=\"270\" src=\"//www.dailymotion.com/embed/video/x6d5uua\" allowfullscreen allow=\"autoplay\"></iframe>"
        }, {
          "id" : "x6cy8y7",
          "title" : "Chelsea vs Paris Saint Germain 1-2 - UCL 2015/2016 (2nd Leg) Highlights (English Commentary) HD",
          "thumbnail_240_url" : "http://s1.dmcdn.net/o4WU8/x240-ZLV.jpg",
          "embed_url" : "http://www.dailymotion.com/embed/video/x6cy8y7",
          "embed_html" : "<iframe frameborder=\"0\" width=\"480\" height=\"270\" src=\"//www.dailymotion.com/embed/video/x6cy8y7\" allowfullscreen allow=\"autoplay\"></iframe>"
        }, {
          "id" : "x6cy7ou",
          "title" : "Barcelona vs Roma 6-1 - UCL 2015/2016 Group Stage Highlights HD",
          "thumbnail_240_url" : "http://s2.dmcdn.net/o4Kll/x240-fX2.jpg",
          "embed_url" : "http://www.dailymotion.com/embed/video/x6cy7ou",
          "embed_html" : "<iframe frameborder=\"0\" width=\"480\" height=\"270\" src=\"//www.dailymotion.com/embed/video/x6cy7ou\" allowfullscreen allow=\"autoplay\"></iframe>"
        }, {
          "id" : "x6cuzbu",
          "title" : "Barcelona vs Chelsea 5-1 (a.e.t.) - UCL 1999/2000 1/4 Final (2nd Leg) Highlights",
          "thumbnail_240_url" : "http://s2.dmcdn.net/o1Y65/x240-h0C.jpg",
          "embed_url" : "http://www.dailymotion.com/embed/video/x6cuzbu",
          "embed_html" : "<iframe frameborder=\"0\" width=\"480\" height=\"270\" src=\"//www.dailymotion.com/embed/video/x6cuzbu\" allowfullscreen allow=\"autoplay\"></iframe>"
        }, {
          "id" : "x6cffog",
          "title" : "Barcelona vs Chelsea 2-1 - UCL 2004/2005 (1st Leg) - Full Highlights",
          "thumbnail_240_url" : "http://s2.dmcdn.net/oxJG8/x240-NZs.jpg",
          "embed_url" : "http://www.dailymotion.com/embed/video/x6cffog",
          "embed_html" : "<iframe frameborder=\"0\" width=\"480\" height=\"270\" src=\"//www.dailymotion.com/embed/video/x6cffog\" allowfullscreen allow=\"autoplay\"></iframe>"
        }, {
          "id" : "x6bt1iz",
          "title" : "Real Madrid vs AC Milan 2-3 - UCL 2009/2010 - Full Highlights (English Commentary)",
          "thumbnail_240_url" : "http://s2.dmcdn.net/onw14/x240-uxB.jpg",
          "embed_url" : "http://www.dailymotion.com/embed/video/x6bt1iz",
          "embed_html" : "<iframe frameborder=\"0\" width=\"480\" height=\"270\" src=\"//www.dailymotion.com/embed/video/x6bt1iz\" allowfullscreen allow=\"autoplay\"></iframe>"
        }, {
          "id" : "x6bt0l9",
          "title" : "FC Barcelona vs Chelsea 1-1 - UCL 2005/2006 2nd Leg - Full Highlights",
          "thumbnail_240_url" : "http://s2.dmcdn.net/onu1H/x240-sd4.jpg",
          "embed_url" : "http://www.dailymotion.com/embed/video/x6bt0l9",
          "embed_html" : "<iframe frameborder=\"0\" width=\"480\" height=\"270\" src=\"//www.dailymotion.com/embed/video/x6bt0l9\" allowfullscreen allow=\"autoplay\"></iframe>"
        }, {
          "id" : "x67dpeq",
          "title" : "Liverpool vs Maribor 3-0 Extended Highlights HD 2017",
          "thumbnail_240_url" : "http://s2.dmcdn.net/nvKBa/x240-oGl.jpg",
          "embed_url" : "http://www.dailymotion.com/embed/video/x67dpeq",
          "embed_html" : "<iframe frameborder=\"0\" width=\"480\" height=\"270\" src=\"//www.dailymotion.com/embed/video/x67dpeq\" allowfullscreen allow=\"autoplay\"></iframe>"
        }, {
          "id" : "x67b8tj",
          "title" : "Napoli vs Manchester City 2-4 Extended Highlights HD 2017",
          "thumbnail_240_url" : "http://s1.dmcdn.net/nvKqZ/x240-gLq.jpg",
          "embed_url" : "http://www.dailymotion.com/embed/video/x67b8tj",
          "embed_html" : "<iframe frameborder=\"0\" width=\"480\" height=\"270\" src=\"//www.dailymotion.com/embed/video/x67b8tj\" allowfullscreen allow=\"autoplay\"></iframe>"
        } ],
        "has_more" : true,
        "page" : 2
      },
      "id" : "req:videos",
      "type" : "videos",
      "baseType" : "videos"
    },
    "dataType" : "custom",
    "nodes" : { },
    "defaultContent" : false,
    "lastModified" : 1530802111901,
    "etag" : "fbf017eb/1530802111901",
    "contentLength" : 0,
    "children" : [ ],
    "totalPages" : 0,
    "outputMode" : "RAW",
    "page" : 1,
    "aggregators" : [ ]
  }
  ... //other fields
}

In order to render the data model, you will have to create the videos template file and put it in the fragments folder of your theme (default theme in the example) of your server. In case you are using the Eclipse SDK:

template-creation
Figure 40. Videos template creation in Eclipse
<div class="videos" />
    <span>Videos</span>
    <#list model.data.videos.list as video>
    <div class="media side-link" >
        <div class="media-left">
          <a href="c">
             <img height="100" width="100" class="media-object" src="${video.thumbnail_240_url!''}" />
          </a>
      </div>
      <div class="media-body">
          <a href="${video.embed_url}">
            <h4 class="media-heading">${video.title}</h4>
          </a>
      </div>
    </div>
    </#list>
</div>

if you navigate again to the url _ssi/fragments/videos it returns:

rendered-page
Figure 41. Rendered page

Since this is supposed to be a fragment you can include it server side, in another template. For example with Freemarker:

<@server_include uri="/_ssi/fragments/videos"/>
ttemplate-server-inclusion
Figure 42. Server side inclusion in section template

when you use server side inclusion of internal content (content generate by cobalt), in the end the html cache (e.g. your cdn) will be unaware that the page has been built from two different pieces, unless you use a mechanism such as ESI, supported partially by Varnish and Akamai

In this case remember to enable ESI on the Site service cobalt.properties with site.templateEngine.esiEnabled=true

5.3. Extending REST APIs

If you need to get output from Site Service API in a different format, or you want to add your custom logic, without adding network overhead, you can add custom API endpoints directly on the Site Service.

Cobalt recognizes endpoints created through the Java API for RESTful Web Services (JAX-RS, defined in JSR 311).

5.3.1. Configuring the Site Service to lookup new JAX-RS Resources

You will have to add to the cobalt.properties file the comma separated list of packages to be scanned in search of new endpoint classes:

common.resource.scan.basePackages=my.scan.package1,my.scan.package2

You need to restart the Cobalt server for the change to take effect.

5.3.2. Example: CustomResource

The JAX-RS resource CustomResource, given the id of a NodeData in Cobalt, retrieves the it, and apply some logic to customize the output, before sending the final response.

CustomResource
package com.eidosmedia.cobalt.extensions;

//imports here

@Path("custom")
@Tag(name = "custom", description = "Custom resource to retrieve a customized view of the articles")
@Produces(MediaType.APPLICATION_JSON)
public class CustomResource extends SiteAwareResource {

        private DataManager dataManager;

        @Inject
        public void setDataManager(DataManager dataManager) {
                this.dataManager = dataManager;
        }

        public static class Custom {
                private String title;
                private Set<String> author;
                private Serializable content;
                public String getTitle() {
                        return title;
                }
                public Set<String> getAuthor() {
                        return author;
                }
                public Serializable getContent() {
                        return content;
                }
                public Custom(String title, Set<String> author, Serializable content) {
                        super();
                        this.title = title;
                        this.author = author;
                        this.content = content;
                }
        }

        @GET
        @Path("{id}")
        public Custom getStory(@PathParam("id") @Parameter(description="The id of the content") NodeId nodeId) {
                try {
                        NodeData node = dataManager.getNodeData(siteKey, nodeId, null, portalRequest);
                        return new Custom(node.getTitle(), node.getAuthors(), node.getContent() );
                } catch (DataNotFoundException ex) {
                        throw new PortalWebEndpointException(ex.getMessage(), ex.getStatusCode(), ErrorEntityType.ENTITY_NOT_FOUND);
                } catch (Exception ex) {
                        throw new PortalWebEndpointException("Unexpected error", Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), ErrorEntityType.ERROR);
                }
        }
}

The package of the CustomResource must be configured among the scan packages:

common.resource.scan.basePackages=package com.eidosmedia.cobalt.extensions

The new REST API answers to the path /custom/{id} as defined by the @Path annotations above the CustomResource class and the getStory method.

Whenever a new HTTP request is made to the API a new instance of CustomResource is automatically created by JAX-RS, which then deserializes the id path parameter into the NodeId instance defined in the getStory method signature and invokes the method.

Extending SiteAwareResource

By extending the parent class SiteAwareResource, a siteKey field, containing the identifier of the site the request is made to, is populated in the class instance.

Remember that the Site service is able to serve multiple web sites, distinguished by their hostnames.
DataManager interface and getNodeData method

The DataManager dependency is injected in the CustomResource instance:

Injecting the DataManager
private DataManager dataManager;

@Inject
public void setDataManager(DataManager dataManager) {
    this.dataManager = dataManager;
}
DataManager is the high level access point to the Cobalt repository. It contains methods to lookup NodeData objects by id, search for objects by their fields or sections and search via full-text queries.

The getNodeData method fetches the NodeData object, identified by NodeId and SiteKey:

NodeData getNodeData(SiteKey siteKey, NodeId nodeId, String[] aggregators, PortalRequest portalRequest) throws DataException

Available aggregator constants are defined in the class com.eidosmedia.portal.dao.Aggregators, and define additional behaviors, when retrieving the NodeData objects.

JSON Response

The API response is the JSON serialized version of the Custom bean, built from the retrieved NodeData, to show only the title, the authors and the content of the node.

{
  "title" : "History's 10 greatest accidental inventors",
  "author" : [ "Matteo Silvestri" ],
  "content" : "<?xml version=\"1.0\" encoding=\"UTF-8\"?><document emk-type=\"story\">...</document>"
}
Handling errors

Errors should be handled throwing com.eidosmedia.portal.api.exceptions.PortalWebEndpointException.

public PortalWebEndpointException(String message, int status, ErrorEntityType error)

status is the HTTP status code for the error, error is a constant describing the error kind.

When serialized, the exception result in a response similar to the following one:

{
  "error" : {
    "message" : "Node with id 0243-094f20b24420-caf9910ef78f-1001 not found in repository",
    "type" : "ENTITY_NOT_FOUND"
  }
}

5.4. Extending the TemplateEngine

The template engine can be extendend by adding your custom functions. From any custom function class, you will be able to inject and use SiteService components as explained in the dependency injection section.

As of version 3.2019.03 of the Cobalt distribution, the default template engine implementation is FreeMarker, an open source Apache project, widely used and well documented.

In FreeMarker, you can implement both the TemplateDirectiveModel and the TemplateMethodModel or TemplateMethodModelEx. Whether to implement a directive or a method depend on the specific needs; you can find a detailed explanation on FreeMarker documentation.

5.4.1. Example: HelloWorldDirective

This example custom directive HelloWorldDirective simply prints a message containing a greeting to the logged in user to the output stream.

package com.eidosmedia.cobalt.extensions;

// imports here

public class HelloWorldDirective implements TemplateDirectiveModel {

        //Injecting the current request proxy
        @Inject
        private PortalRequest pr;

        @Override
        public void execute(Environment env, @SuppressWarnings("rawtypes") Map params, TemplateModel[] loopVars, TemplateDirectiveBody body)
                        throws TemplateException, IOException {
                //getting the output writer
                Writer o = env.getOut();
                //if is there a user logged in
                if (pr!=null && pr.getUser() != null) {
                        o.write("Hello ");
                        o.write(pr.getUser().getAlias());
                } else {
                        o.write("No user logged in");
                }
        }

}

The injected PortalRequest contains information on the current request, similarly to the HttpServletRequest, but including also information specific to Cobalt, such as the current user:

UserData getUser();

The directive must be registered in the cobalt.properties:

site.templateEngine.function[0].class=com.eidosmedia.cobalt.extensions.HelloWorldDirective
site.templateEngine.function[0].key=hello

After restarting Cobalt, the directive can be used in FreeMarker templates:

<@hello />

The same configuration can be provided to the docker container as environment variable. For example in docker-compose syntax:

environment:
    - "site.templateEngine.function[0].class=com.eidosmedia.cobalt.extensions.HelloWorldDirective"
    - "site.templateEngine.function[0].key=hello"

6. Sanitizing URLs

The UrlSanitizer interface implementations intercept each incoming http request and allow to redirect the request to a new url applying custom sanitization logic:

/**
 * Sanitize request if needed.
 */
public interface UrlSanitizer {

    /**
     * Return a sanitized redirect URI and status code if needed.
     *
     * @param request
     * @return the sanitized redirect or <code>null</code> if there is no need to sanitize the URI.
     */
    Redirect sanitize(RequestURI request);
}

The sanitize method receives a RequestURI instance, containing all data related to the current request URI:

public class RequestURI {

    ...

    public String getScheme() {
        return scheme;
    }

    public String getHostname() {
        return hostname;
    }

    public int getPort() {
        return port;
    }

    public String getPath() {
        return path;
    }

    public Map<String, String[]> getQueryParameters() {
        return queryParameters;
    }

    public String getQueryString() {
        return queryString;
    }

    ...
}

It must return a Redirect object if the request must be redirected to a sanitized URI or null otherwise:

public class Redirect {

    private final int statusCode;
    private final String uri;

    public Redirect(int statusCode, String uri) {
        this.statusCode = statusCode;
        this.uri = uri;
    }

    public Redirect(String uri) {
        this.statusCode = 301;
        this.uri = uri;
    }

    public int getStatusCode() {
        return statusCode;
    }

    public String getUri() {
        return uri;
    }

}

Status code 301 Moved Permanently is used as default, or you can provide your 3XX code in the constructor.

6.1. Configuration

In order to register a UrlSanitizer in the Site Service configuration, you just need to add the line:

site.url.sanitizer=com.eidosmedia.portal.url.sanitizer.PrettyUrlSanitizer

to the cobalt.properties file providing the fully qualified name of your implementation.

The same configuration can be provided to the docker container as environment variable. For example in docker-compose syntax:

environment:
    - "site.url.sanitizer=com.eidosmedia.portal.url.sanitizer.PrettyUrlSanitizer"

6.2. Example: PrettyUrlSanitizer

The following example implementation, available in the Cobalt package, just replaces repeated slashes and makes the URI lowercase:

public class PrettyUrlSanitizer implements UrlSanitizer {

    @Override
    public Redirect sanitize(RequestURI request) {
        String path = request.getPath()
                .replaceAll("/{2,}", Matcher.quoteReplacement("/"))
                .toLowerCase();
        String hostname = request.getHostname();
        if (!hostname.startsWith("www.")) {
            hostname = "www." + hostname;
        }

        RequestURI newURI = new RequestURI(request.getScheme(), hostname, request.getPort(), path,
                                           request.getQueryParameters(), request.getQueryString());
        if (!newURI.equals(request)) {
            return new Redirect(newURI.toString());
        } else {
            return null;
        }
    }

}

You can extend the directory service adding custom connectors of actions.

6.3. Connector

A connector allow you to configure links to external accounts/providers used, for example, to perform login in place of standard username and password authentication.

6.3.1. Example: Local Connector

The resource class (eg: GlobeResource) contains an implementation of method getConnectorKey already filled by the archetype. Normally in this class you don’t need to do any implementation, the only case when you need to do so, is when you have to override the default login method implemented in the super class ConnectorResource, but with this operation you will lose the default busisness logic introducing,potential side effects.

package com.eidosmedia.cobalt.connector;

import javax.ws.rs.Path;

import com.eidosmedia.portal.directory.webresources.connectors.ConnectorResource;

@Path("/connectors/globe")
public class GlobeResource extends ConnectorResource<GlobeConnectorData> {

    @Override
    protected String getConnectorKey() {
            return GlobeConnector.KEY;
    }
}

The connector class (eg: GlobeConnector) handle the comunication between Cobalt and the external provider. In this class you have to implement the method getUser(GlobeConnectorData data) writing the logic for authenticate to the external provider (eg: calling an API or a web service). In this class you can put some properties that will be evaluated during the reading of the configuration file through the relative setters.

public class GlobeConnector implements Connector<GlobeConnectorData>, Configurable {

    @Inject
    private Client client;

    public static final String KEY = "globe";

    /**
     * IF YOUR CONNECTOR NEEDS PROPERTIES FROM THE XML CONFIGURATION YOU MUST CREATE GETTER AND SETTERS METHODS OF EACH PROPERTY.
     */

    public void init() throws InitializationException {
        //Place init logic here
    }

    public void loadConfiguration(HierarchicalConfiguration configuration) throws ConfigurationException {
        //Place your custom (if any) configurations loading here, this method is called during the directory module boot.
    }

    public void destroy() throws DestructionException {
        //Place destroy logic here
    }

    @Override
    public ConnectorUser getUser(GlobeConnectorData data) throws ConnectionException {

                // This method must return an instance of object connectoruser.
                // The logic could be the following:
                // 1 - call the third party api (eg: via jersey client) in order to get the user
                // info.
                // 2 - pass the values returned to the connectoruser constructor.
                Map<String, String> response;
                Form form = new Form();
                form.param("name", data.getName())
                .param("password", data.getPassword());

                Entity<Form> entity = Entity.form(form);
                try {
                        response = client.target("http://localhost:3000/login").request()
                                        .post(entity, new GenericType<Map<String, String>>() {
                                        });
                } catch (WebApplicationException ex) {
                        throw new ConnectionException("error", ex.getMessage(),
                                        Response.Status.fromStatusCode(ex.getResponse().getStatus()), ErrorEntityType.ERROR);
                } catch (Exception ex) {
                        throw new ConnectionException("error", ex.getMessage(), Response.Status.INTERNAL_SERVER_ERROR,
                                        ErrorEntityType.ERROR);
                }
                return new ConnectorUser(response.get("id"), response.get("token"), response.get("username"),
                                response.get("name"), response.get("surname"), response.get("name") + " " + response.get("surname"),
                                response.get("email"), null);
    }

    @Override
    public Status getCreationStatusEnum() {
        return Status.ENABLED;
    }

    @Override
    public String getKey() {
        return KEY;
    }

    @Override
    public Class<? extends ConnectorResource<GlobeConnectorData>> getConnectorResourceType() {
        return GlobeResource.class;
    }

    @Override
    public String getName() {
        return "Globe";
    }

    @Override
    public String evalUserName(ConnectorUser user) {
        return user.getUsername();
    }

    @Override
    public String evalAlias(ConnectorUser user) {
        return user.getUsername();
    }

    @Override
    public void validate(GlobeConnectorData data) throws WebApplicationException {
        Objects.requireNonNull(data);
    }

    @Override
    public ConnectorInfo getInfo() {
        return new ConnectorInfo();
    }

        @Override
        public ConnectorUser getUser(GlobeConnectorData arg0, UserData arg1) throws ConnectionException {
                return null;
        }

}

The data class (eg: GlobeConnectorData) is a bean that contains some properties used by the connector.

public class GlobeConnectorData implements ConnectorData {

        private String name;
        private String password;

        public String getName() {
                return name;
        }

        public void setName(String name) {
                this.name = name;
        }

        public String getPassword() {
                return password;
        }

        public void setPassword(String password) {
                this.password = password;
        }

}
Configuration

To use the new connector you have to configure your instance of the Directory service, the files to change are the following:

  • directory.xml

  • context.xml

Into the directory.xml locate the section Connectors if you cannot find this section feel free to create one. The configuration is the similar to the following:

<DirectoryService>
...
    <Connectors>
        <Connector
                em-type="com.eidosmedia.cobalt.connector.GlobeConnector"
                property="value" property2="value2" />
    </Connectors>
...
</DirectoryService>

In order to make you Tomcat aware of the new connector, after the export of the compiled code into a JAR file (eg: globeConnector.jar) you have to edit the context.xml (the EXPORT_FOLDER placeholder must point to the folder where you have exported the JAR file):

<Context>
...
    <Resources className="org.apache.catalina.webresources.StandardRoot">
        <PreResources className="org.apache.catalina.webresources.DirResourceSet"
            base="/EXPORT_FOLDER/lib" internalPath="/" webAppMount="/WEB-INF/lib" />
    </Resources>
...
</Context>

The use of PreResource is documented at this page: https://tomcat.apache.org/tomcat-8.0-doc/config/resources.html

6.4. Action

An action allow you to configure a custom operation that will be executed after the call of some directory APIs (at the moment the supported APIs are: users/create, users/register, users/update). An action for example can be a called after an update to align an external database with the updated user.

6.4.1. Example: Local Action

The class (eg: GlobeAction) handle the business logic that will applied after Cobalt API execution. Contains an implementation of method getKey already filled by the archetype.

In this class you can put some properties that will be evaluated during the reading of the configuration file through the relative setters.

In this class you have to implement the method execute(UserData data, HttpServletRequest request) writing the logic to execute after API (eg: calling an API or a web service). The data object must be an object that extends the AuditData class so you can create actions that uses the following implementations:

  • UserData

  • GroupData

  • SessionData

  • AvatarData

The execute method returns a boolean that will be set to true if the action was executed correctly otherwise false will be returned in case of failure.

In case of failure the action will be stored and will be executed in a second time (see the Configuration section for more information).

package com.eidosmedia.portal.directory.webresources.actions;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.configuration.HierarchicalConfiguration;

import com.eidosmedia.portal.DestructionException;
import com.eidosmedia.portal.InitializationException;
import com.eidosmedia.portal.configuration.ConfigurationException;
import com.eidosmedia.portal.directory.UserData;

public class GlobeAction extends Action<UserData> {
    private static final String KEY = "GlobeAction";

    @Override
    public void init() throws InitializationException {
        //Place init logic here
    }

    @Override
    public void destroy() throws DestructionException {
        //Place destroy logic here
    }

    @Override
    public void loadConfiguration(HierarchicalConfiguration configuration) throws ConfigurationException {
        //Place configuration loading logic here
    }

    @Override
    public boolean execute(UserData userData, HttpServletRequest request) throws ActionException {
        return false;
    }

    @Override
    public String getKey() {
        return KEY;
    }

}
Configuration

To use the new action you have to configure your instance of the Directory service, the files to change are the following:

  • directory.xml

  • context.xml

Into the directory.xml locate the section Actions if you cannot find this section feel free to create one. The configuration is the similar to the following:

<DirectoryService>
...
    <Actions failedActionsCron="*/30 * * * * ?" failedActionsMaxRetries="5">
        <Action
        em-type="com.eidosmedia.cobalt.action.GlobeAction" trigger="UPDATE_USER", property="value" property2="value2"/>
    </Actions>
...
</DirectoryService>

Each action must specify the trigger attribute, that is used to link the action the the correponding API. At the moment the allowed values are the following (more will be introduced):

  • CREATE_USER

  • UPDATE_USER

  • REGISTER_USER

The attributes failedActionsCron and failedActionsMaxRetries are used in case of failed action, the first attributes using a cron expression sets the interval when the failed action will be executed again, the second attribute sets the number of max retries for each failed action.

In order to make you Tomcat aware of the new action, after the export of the compiled code into a JAR file (eg: globeAction.jar) you have to edit the context.xml (the EXPORT_FOLDER placeholder must point to the folder where you have exported the JAR file):

<Context>
...
    <Resources className="org.apache.catalina.webresources.StandardRoot">
        <PreResources className="org.apache.catalina.webresources.DirResourceSet"
            base="/EXPORT_FOLDER/lib" internalPath="/" webAppMount="/WEB-INF/lib" />
    </Resources>
...
</Context>

The use of PreResource is documented at this page: https://tomcat.apache.org/tomcat-8.0-doc/config/resources.html