Xorcery Examples - Forum Application

 



This is another example of Xorcery using commands/aggregates, including JAX-RS integration, along with a simple forum application domain example (posts with comments). It now shows the full cycle of GET, submit form, parse/process command, turn into events, project into Neo4j, query updates from Neo4j, and render with updated data.


Setup


We will use the same dependencies from our first Greeter example, so we will only add those that are additional and necessary for this example.

    <!-- Domain model and projection -->
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-domainevents-publisher</artifactId>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-domainevents-neo4jprojection</artifactId>
    </dependency>

These dependencies are necessary for the projection to neo4j.

      <!-- REST API -->
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-jsonapi-server-neo4j</artifactId>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-domainevents-jsonapi</artifactId>
    </dependency>

We add these to the REST-API dependencies to support domain events and projection to neo4j.

 

    <!-- Integration -->
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-dns-registration</artifactId>
    </dependency>

In the integration dependencies we only add this for registration in the DNS server.


     <!-- Logging -->
     <dependency>
          <groupId>com.exoreaction.xorcery</groupId>
          <artifactId>xorcery-log4j</artifactId>
      </dependency>

We added the logging dependency for Xorcery to be able to see the messages that Xorcery publishes in the log.

    <!-- These features can be extracted out into their own service -->
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-jwt-server</artifactId>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-eventstore</artifactId>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-opensearch</artifactId>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-certificates-ca</artifactId>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-dns-server</artifactId>
    </dependency>

Finally, we publish the dependencies that allow us to work with a JWT server, an event store, an open search, and a DNS server. What makes Xorcery so special is that it establishes an almost real setup for working with microservices.

In the src/main/docker folder, you will find a docker-compose.yml that starts precisely these dependent services.

version: "3.9"
services:
  exmples-forum-eventstore:
    image: eventstore/eventstore:23.6.0-buster-slim
    environment:
      - EVENTSTORE_CLUSTER_SIZE=1
      - EVENTSTORE_RUN_PROJECTIONS=All
      - EVENTSTORE_START_STANDARD_PROJECTIONS=true
      - EVENTSTORE_EXT_TCP_PORT=1113
      - EVENTSTORE_HTTP_PORT=2113
      - EVENTSTORE_INSECURE=true
      - EVENTSTORE_ENABLE_EXTERNAL_TCP=true
      - EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP=true
      - EVENTSTORE_MAX_APPEND_SIZE=8388608
    deploy:
      resources:
        limits:
          cpus: "2"
          memory: 2G
        reservations:
          cpus: "1"
          memory: 1G
    ports:
      - "1113:1113"
      - "2113:2113"
    volumes:
      - type: volume
        source: eventstore-volume-data
        target: /var/lib/eventstore
      - type: volume
        source: eventstore-volume-logs
        target: /var/log/eventstore
  exmples-forum-opensearch:
    image: opensearchproject/opensearch:2.11.0
    environment:
      - cluster.name=opensearch-cluster
      - node.name=opensearch-node1
      - bootstrap.memory_lock=true # along with the memlock settings below, disables swapping
      - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" # minimum and maximum Java heap size, recommend setting both to 50% of system RAM
      - "DISABLE_INSTALL_DEMO_CONFIG=true" # disables execution of install_demo_configuration.sh bundled with security plugin, which installs demo certificates and security configurations to OpenSearch
      - "DISABLE_SECURITY_PLUGIN=true" # disables security plugin entirely in OpenSearch by setting plugins.security.disabled: true in opensearch.yml
      - "discovery.type=single-node" # disables bootstrap checks that are enabled when network.host is set to a non-loopback address
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536 # maximum number of open files for the OpenSearch user, set to at least 65536 on modern systems
        hard: 65536
    volumes:
      - opensearch-data1:/usr/share/opensearch/data
    ports:
      - 9200:9200
      - 9600:9600 # required for Performance Analyzer
  exmples-forum-opensearch-dashboards:
    image: opensearchproject/opensearch-dashboards:2.11.0
    ports:
      - 5601:5601
    expose:
      - "5601"
    environment:
      - 'OPENSEARCH_HOSTS=["http://exmples-forum-opensearch:9200"]'
      - "DISABLE_SECURITY_DASHBOARDS_PLUGIN=true" # disables security dashboards plugin in OpenSearch Dashboards
volumes:
  eventstore-volume-data:
  eventstore-volume-logs:
  opensearch-data1:
networks:
  bridge:
    driver: bridge


Note: You have to have docker desktop installed and previously run this docker-compose and then run it in the Main class of the project.


Now we add the Xorcery configuration and the Neo4j framework of POST and COMMENTS.


In this project, we can see the configuration of logging, rest API resources, certificates, dns client configuration, and jwt server:

application.name: "forum"
instance.home: "{{ SYSTEM.jpackage_app-path ? jpackage.app | SYSTEM.user_dir}}"
jpackage.app: "{{ SYSTEM.jpackage_app-path }}/../../lib/app"
# So that we can generate a SSL certificate for the local hostname. Replace with whatever domain name you actually use
instance.domain: local
# Add local convenience names for your own computer into the SSL cert
certificates:
  dnsNames:
    - localhost
    - "{{ instance.host }}"
  ipAddresses:
    - 127.0.0.1
    - "{{ instance.ip }}"
# REST API resources
jersey.server.register:
  - com.exoreaction.xorcery.examples.forum.resources.api.CommentResource
  - com.exoreaction.xorcery.examples.forum.resources.api.ForumResource
  - com.exoreaction.xorcery.examples.forum.resources.api.PostCommentsResource
  - com.exoreaction.xorcery.examples.forum.resources.api.PostResource
  - com.exoreaction.xorcery.examples.forum.resources.api.PostsResource

dns.client.search:
  - xorcery.test
dns.client.hosts:
      _certificates._sub._https._tcp : "https://127.0.0.1"
dns.client.nameServers:
  - 127.0.0.1:8853
jetty:
  server:
    http:
      port: 8080
    ssl:
      port: 8443
    security:
      jwt:
        issuers:
          server.xorcery.test:
            keys:
            - kid: "2d3f1d1f-4038-4c01-beb7-97b260462ada"
              alg: "ES256"
              publicKey: "secret:MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEd7L6zz97U1MMaj9MSN325SZ15htR26mec0/1A0vt1b8Yfcu0QuiN9E4ijSfMRiof+B57P/hkrb+OuRSYLL854Q=="
# These features can be extracted into separate services
jwt.server.keys:
  - kid: "2d3f1d1f-4038-4c01-beb7-97b260462ada"
    alg: "ES256"
    publicKey: "secret:MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEd7L6zz97U1MMaj9MSN325SZ15htR26mec0/1A0vt1b8Yfcu0QuiN9E4ijSfMRiof+B57P/hkrb+OuRSYLL854Q=="
    privateKey: "secret:MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCCSHC362NTeBZYTkYGXK3vfRvoqQum+Uo6DFUDzvX7MuA=="
dns.server.port: 8853
# Log configuration
log4j2.Configuration:
  name: Xorcery Example Forum
  status: warn
  thresholdFilter:
    level: trace
  appenders:
    Console:
      name: STDOUT
      target: SYSTEM_OUT
      PatternLayout:
        Pattern: "%d [%t] %-5level %marker %c{1.}: %msg%n%throwable"
#    Log4jPublisher:
#      name: Log4jPublisher
#      PatternLayout:
#        Pattern: "%d [%t] %-5level %marker %c{1.}: %msg%n%throwable"
  Loggers:
    logger:
      - name: org.apache.logging.log4j
        level: debug
        additivity: false
        AppenderRef:
          ref: STDOUT
      - name: com.exoreaction.xorcery.core.Xorcery
        level: debug
      - name: com.exoreaction.xorcery.service
        level: debug
      - name: com.exoreaction.xorcery.dns
        level: trace
    Root:
      level: info
      AppenderRef:
        - ref: STDOUT
 #       - ref: Log4jPublisher

We verify that the setup starts without problems by executing the Main class.


public class Main {
  public static void main(String[] args ) throws Exception
  {
    System.exit(new CommandLine(new com.exoreaction.xorcery.runner.Main()).execute(args));
  }
}

If everything is fine this will be the result.



We are done with the setup.


Forum Service

We register the ForumService and inject the configuration and service resource objects.

@Service(name="forum")
@RunLevel(20)
public class ForumService {
    @Inject
    public ForumService(Configuration configuration, ServiceResourceObjects serviceResourceObjects) {
        serviceResourceObjects.add(new ServiceResourceObject.Builder(new InstanceConfiguration(configuration.getConfiguration("instance")), "forum")
                .version("1.0.0")
                .attribute("domain", "forum")
                .api("forum", "api/forum")
                .build());
    }
}


Model

We create the model for the application by inheriting from Xorcery base classes. For example, the ForumModel inherits from CommonModel.

CommonModel

package com.exoreaction.xorcery.domainevents.helpers.model;

public interface CommonModel {
  public static enum Entity {
    id,
    aggregateId,
    externalId,
    createdOn,
    lastUpdatedOn;

    private Entity() {
    }
  }

  public static enum Label {
    Entity,
    Aggregate;

    private Label() {
    }
  }
}

 ForumModel

package com.exoreaction.xorcery.examples.forum.model;
import com.exoreaction.xorcery.domainevents.helpers.model.CommonModel;
public interface ForumModel
        extends CommonModel {
    enum Label {
        Post,
        Comment
    }
    enum Relationship {
        PostComments
    }
    enum Post {
        title,
        body,
        is_comments_enabled
    }
    enum Comment {
        body
    }
}


Now we create the model for Post and Comments, they both inherit from EntityModel:

package com.exoreaction.xorcery.domainevents.helpers.model;
import com.exoreaction.xorcery.domainevents.helpers.model.CommonModel.Entity;
public interface EntityModel extends Model { // Model inherit from JsonElement
  default String getId() {
    return (String)this.getString(Entity.id).orElse((Object)null);
  }
  default String getAggregateId() {
    return (String)this.getString(Entity.aggregateId).orElse((Object)null);
  }
}

PostModel

public record PostModel(ObjectNode json)
        implements EntityModel {
    public String getTitle() {
        return getString(ForumModel.Post.title).orElse("");
    }
    public String getBody() {
        return getString(ForumModel.Post.body).orElse("");
    }
}

CommentsModel

public record CommentModel(ObjectNode json)
        implements EntityModel {
    public String getBody() {
        return getString(ForumModel.Comment.body).orElse("");
    }
}

Finally, the records that use the neo4j client to obtain information from the posts or comments.


Posts

public record Posts(GraphDatabase db) {
    private static final String POSTS = MessageFormat.format(
            "MATCH ({0}:{0}) WITH {0}, {0} as {1}",
            ForumModel.Label.Post, CommonModel.Label.Entity);
    private final static BiConsumer<GraphQuery, StringBuilder> clauses = where()
            .parameter(CommonModel.Entity.id, String.class, "Post.id=$entity_id");
    public GraphQuery posts() {
        return db.query(POSTS).where(clauses);
    }
}

Comments

public record Comments(GraphDatabase db) {
    private static final String COMMENTS = MessageFormat.format(
            "MATCH ({0}:{0}) WITH {0}, {0} as {1}",
            ForumModel.Label.Comment, CommonModel.Label.Entity);
    private final static BiConsumer<GraphQuery, StringBuilder> clauses = where()
            .parameter(CommonModel.Entity.id, String.class, "Comment.id=$entity_id");
    private static final String POST_COMMENTS = MessageFormat.format(
            "MATCH ({0}:{0})-[:{1}]->({2}:{2}) WITH {2}, {2} as {3}",
            ForumModel.Label.Post, ForumModel.Relationship.PostComments, ForumModel.Label.Comment, CommonModel.Label.Entity);
    private final static BiConsumer<GraphQuery, StringBuilder> byPostClauses = where()
            .parameter(CommonModel.Entity.id, String.class, "Post.id=$entity_id");
    public GraphQuery comments() {
        return db.query(COMMENTS).where(clauses);
    }
    public GraphQuery commentsByPost(String postId)
    {
        return db.query(POST_COMMENTS).where(byPostClauses).parameter(CommonModel.Entity.id, postId);
    }
}

Entities

This is where we create, and update the posts. Likewise, the creation, updating, and deletion of comments occur. In summary, the events are generated according to the generated commands.

PostEntity

public class PostEntity
        extends Entity<PostEntity.PostSnapshot> {
    @Create
    public record CreatePost(String id, String title, String body)
            implements Command {
    }
    @Update
    public record UpdatePost(String id, String title, String body)
            implements Command {
    }
    public static class PostSnapshot
            implements EntitySnapshot {
        public String title;
        public String body;
    }
    public void handle(CreatePost command) {
        add(event("createdpost")
                .created("Post", command.id)
                .attribute("title", command.title)
                .attribute("body", command.body)
                .build());
    }
    public void handle(UpdatePost command) {
        add(event("updatedpost")
                .updated("Post", command.id)
                .attribute("title", command.title)
                .attribute("body", command.body)
                .build());
    }
}

CommentEntity

public class CommentEntity
        extends Entity<CommentEntity.CommentSnapshot> {
    @Create
    public record AddComment(String id, String body)
            implements Command {
    }
    @Update
    public record UpdateComment(String id, String body)
            implements Command {
    }
    @Delete
    public record RemoveComment(String id)
            implements Command {
    }
    public static class CommentSnapshot
            implements EntitySnapshot {
        public String body;
    }
    public void handle(AddComment command) {
        add(event("addedcomment")
                .created("Comment", command.id)
                .attribute("body", command.body)
                .addedRelationship("PostComments", "Post", metadata.getAggregateId())
                .build());
    }
    public void handle(UpdateComment command) {
        if (snapshot.body.equals(command.body))
            return;
        add(event("updatedcomment")
                .updated("Comment", command.id)
                .attribute("body", command.body)
                .build());
    }
    public void handle(RemoveComment command) {
        add(event("removedcomment").deleted("Comment", command.id));
    }
}

ForumApplication

In this service, we inject the DomainEventPublisher, the GraphDatabae, and the ServiceLocator. We also establish the DomainContext for Post and Comments.

@Service(name="forum")
public class ForumApplication {
    private static final Logger logger = LogManager.getLogger(ForumApplication.class);

    private final DomainEventPublisher domainEventPublisher;
    private final Neo4jEntitySnapshotLoader snapshotLoader;

    private final Supplier<PostEntity> postEntitySupplier;
    private final Supplier<CommentEntity> commentEntitySupplier;

    @Inject
    public ForumApplication(DomainEventPublisher domainEventPublisher,
                            GraphDatabase database,
                            ServiceLocator serviceLocator
    ) {
        this.domainEventPublisher = domainEventPublisher;
        this.snapshotLoader = new Neo4jEntitySnapshotLoader(database);
        postEntitySupplier = () -> serviceLocator.createAndInitialize(PostEntity.class);
        commentEntitySupplier = () -> serviceLocator.createAndInitialize(CommentEntity.class);
    }

    public PostsContext posts() {
        return new PostsContext(this, postEntitySupplier);
    }

In this class, the publication of command events is handled generically.

  public <T extends EntitySnapshot> CompletionStage<Metadata> handle(Entity<T> entity, DomainEventMetadata metadata, Command command) {

        try {
            DomainEventMetadata domainMetadata = new DomainEventMetadata.Builder(metadata.context())
                    .domain("forum")
                    .commandName(command.getClass())
                    .build();

            T snapshot;

            if (Command.isCreate(command.getClass())) {
                // Should fail
                try {
                    snapshotLoader.load(domainMetadata, command.id(), entity);
                    return CompletableFuture.failedStage(new BadRequestException("Entity already exists"));
                } catch (Exception e) {
                    // Good!
                    Class<?> snapshotClass = (Class<?>) ((ParameterizedType) entity.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
                    snapshot = (T)snapshotClass.getConstructor().newInstance();
                }

            } else {
                snapshot = snapshotLoader.load(domainMetadata, command.id(), entity);
            }

            List<DomainEvent> events = entity.handle(domainMetadata, snapshot, command);

            return domainEventPublisher.publish(new CommandEvents(metadata.context(), events));
        } catch (Throwable e) {
            return CompletableFuture.failedStage(e);
        }
    }
}

REST API Resources

The REST API Resources inherit from Xorcery's JsonApiResource and implement some Mixin for extra functionality.

# REST API resources
jersey.server.register:
  - com.exoreaction.xorcery.examples.forum.resources.api.CommentResource
  - com.exoreaction.xorcery.examples.forum.resources.api.ForumResource
  - com.exoreaction.xorcery.examples.forum.resources.api.PostCommentsResource
  - com.exoreaction.xorcery.examples.forum.resources.api.PostResource
  - com.exoreaction.xorcery.examples.forum.resources.api.PostsResource


Test

After la execution of the Main class:


Let's do clic in /api/forum:


The form below shows how to use the URI template, instead of having to create the URL manually:


I invite you to try this demo and leave us your comments. Remember that the source code is found at https://github.com/cantara/xorcery-examples.


Enjoy!

Joe

Share:

Xorcery Ejemplos - Greeter

 


Vamos a explicar nuestro primer ejemplo con Xorcery llamado Greeter, que encontraréis en el siguiente repositorio de Github.

Con Xorcery podemos implementar la API de servicio como una API REST (usando JSON-API como tipo de contenido) para necesidades de request/response o websockets de transmisión reactiva (publicadores de servidores o suscriptores) para necesidades de transmisión (como abastecimiento de eventos o proyecciones o colección de logs, etcétera).


Dato importante: toda la implementación de Xorcery utiliza las API de Jakarta EE y bibliotecas importantes en el mundo Java. Xorcery, por ejemplo, utiliza HK2, un marco de inyección dinámico y liviano.


¿Por qué JSON:API?



La elección en JSON-API y JSONSchema es que permite crear un cliente REST, y no un cliente HTTP, que las aplicaciones pueden utilizar.

Cuando se hace correctamente, se llega a la situación de que todas las interacciones del código de la aplicación con los clientes REST son una de estas dos: seguir enlaces y enviar formularios. Eso es todo. No es necesario construir URLs, ni averiguar qué método usar, ni nada de eso. Todo esto sucede detrás de escena, tal como lo define JSON:API y JSONSchema en el servidor. 

El código de la aplicación solo necesita preocuparse en los enlaces que le interesan, qué información necesita extraer de los recursos en JSON: API y cómo enviar formularios (que conocen el URI y el método a usar).

Actualmente, admitimos plantillas de URI en los esquemas JSON:API y en el entorno de prueba HTML. De esta manera es más fácil probar la API en un navegador, además de simplificar el llenado de las URL como un formulario.



¿Por qué Jetty WebSockets + Disruptor?



Xorcery crea una variación personalizada de la API ReactiveStreams con WebSockets para enviar eventos como encabezados más bytes, así como una integración con la API Disruptor. En el código fuente de xorcery encontrará servicios donde se aplica la arquitectura, como editor+suscriptor, editor de eventos de métricas+suscriptor y editor de eventos de dominio+suscriptor.


Greeter


En el repositorio de GitHub, encontrará ejemplos de xorcery donde se ha creado un proyecto modular.

Aquí encontrará el BOM, las dependencias comunes y los plugins.

 <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.release>17</maven.compiler.release>

    <xorcery.version>0.79.2</xorcery.version>
    <hk2.version>3.0.5</hk2.version>
    <jersey.version>3.1.3</jersey.version>
    <slf4j.version>2.0.7</slf4j.version>
    <log4j.version>2.22.0</log4j.version>

    <junit.version>5.10.0</junit.version>
    <junit.platform.version>1.9.0</junit.platform.version>
  </properties>


  <modules>
    <module>xorcery-examples-greeter</module>
  </modules>


   <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.exoreaction.xorcery</groupId>
        <artifactId>xorcery-bom</artifactId>
        <version>${xorcery.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-bom</artifactId>
        <version>${log4j.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>org.hamcrest</groupId>
        <artifactId>hamcrest</artifactId>
        <version>2.2</version>
        <scope>test</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
Los plugins:

<build>
    <pluginManagement>
      <plugins>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.11.0</version>
          <configuration>
            <annotationProcessorPaths>
              <path>
                <groupId>org.glassfish.hk2</groupId>
                <artifactId>hk2-metadata-generator</artifactId>
                <version>${hk2.version}</version>
              </path>
            </annotationProcessorPaths>
          </configuration>
        </plugin>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-dependency-plugin</artifactId>
          <version>3.6.1</version>
        </plugin>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>3.2.2</version>
          <dependencies>
            <dependency>
              <groupId>me.fabriciorby</groupId>
              <artifactId>maven-surefire-junit5-tree-reporter</artifactId>
              <version>1.2.1</version>
            </dependency>
          </dependencies>
          <configuration>
            <reportFormat>plain</reportFormat>
            <consoleOutputReporter>
              <disable>true</disable>
            </consoleOutputReporter>
            <statelessTestsetInfoReporter
              implementation="org.apache.maven.plugin.surefire.extensions.junit5.JUnit5StatelessTestsetInfoTreeReporter"/>
          </configuration>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>

Es importante que coloques los repositorios de Cantara y el DistributionManagement para que las dependencias de Xorcery se puedan descargar sin problemas.

  <repositories>
    <repository>
      <id>cantara-releases</id>
      <name>Cantara Release Repository</name>
      <url>https://mvnrepo.cantara.no/content/repositories/releases/</url>
    </repository>
    <repository>
      <id>cantara-snapshots</id>
      <name>Cantara Snapshot Repository</name>
      <url>https://mvnrepo.cantara.no/content/repositories/snapshots/</url>
    </repository>
  </repositories>

  <distributionManagement>
    <repository>
      <id>cantara</id>
      <name>Cantara Release Repository</name>
      <url>https://mvnrepo.cantara.no/content/repositories/releases/</url>
    </repository>
    <snapshotRepository>
      <id>cantara</id>
      <name>Cantara Snapshot Repository</name>
      <url>https://mvnrepo.cantara.no/content/repositories/snapshots/</url>
    </snapshotRepository>
  </distributionManagement>


El informe de prueba se presenta en una elegante salida gracias a https://github.com/fabricorby/maven-surefire-junit5-tree-reporter.


Implementación de Greeter

Este proyecto tiene a  xorcery-examples como su proyecto padre. 

<parent>
    <groupId>com.exoreaction.xorcery.examples</groupId>
    <artifactId>xorcery-examples</artifactId>
    <version>1.0-SNAPSHOT</version>
</parent>


Agregamos las dependencias core:

        <!-- Core dependencies -->
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-core</artifactId>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-runner</artifactId>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-metadata</artifactId>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-configuration-api</artifactId>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-json</artifactId>
    </dependency>

Luego las dependencias para API REST:

  <!-- REST API -->
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-jsonapi-jaxrs</artifactId>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-jersey-server</artifactId>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-handlebars</artifactId>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-status-server</artifactId>
    </dependency>


Xorcery trabaja con Jetty y Jersey como implementación de Jakarta JAX-RS.

Las dependencias para integración de servicios e implementación de reactive streams:

<!-- Integration -->
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-jersey-client</artifactId>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-reactivestreams-client</artifactId>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-reactivestreams-server</artifactId>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-neo4j</artifactId>
      <scope>compile</scope>
      <exclusions>
        <exclusion>
          <groupId>org.glassfish.jersey.containers</groupId>
          <artifactId>jersey-container-servlet</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-domainevents-neo4jprojection</artifactId>
    </dependency>
Dependencias para Logging:


  <!-- Logging -->
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-log4j</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-core</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-slf4j2-impl</artifactId>
    </dependency>

Integración con JUnit:

   <!-- Test -->
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-junit</artifactId>
      <scope>test</scope>
    </dependency>

Finalmente, agregamos el profile para usar  jpackage que fue introducido en Java 14, que nos permite crear la imagen nativa de una aplicación e instalar esta.

<profiles>
    <profile>
      <id>jpackage</id>
      <activation>
        <os>
          <name>linux</name>
          <arch>amd64</arch>
        </os>
      </activation>
      <build>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-dependency-plugin</artifactId>
            <version>3.6.1</version>
                        <executions>
              <execution>
                <id>copy-dependencies</id>
                <phase>package</phase>
                <goals>
                  <goal>copy-dependencies</goal>
                </goals>
                <configuration>
                  <includeScope>compile</includeScope>
                  <outputDirectory>${project.build.directory}/app/lib</outputDirectory>
                  <overWriteReleases>false</overWriteReleases>
                  <overWriteSnapshots>true</overWriteSnapshots>
                  <overWriteIfNewer>true</overWriteIfNewer>
                </configuration>
              </execution>
            </executions>
          </plugin>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-antrun-plugin</artifactId>
            <version>3.1.0</version>
                        <executions>
              <execution>
                <id>copy-modularized-jar</id>
                <phase>package</phase>
                <goals>
                  <goal>run</goal>
                </goals>
                <configuration>
                  <target>
                    <copy file="${project.build.directory}/${project.build.finalName}.jar"
                      tofile="${project.build.directory}/app/lib/${project.build.finalName}.jar"
                      overwrite="true"/>
                  </target>
                </configuration>
              </execution>
            </executions>
          </plugin>
          <plugin>
            <artifactId>maven-resources-plugin</artifactId>
            <version>3.3.1</version>
            <executions>
              <execution>
                <id>copy-app-resources</id>
                <phase>package</phase>
                <goals>
                  <goal>copy-resources</goal>
                </goals>
                <configuration>
                  <outputDirectory>${basedir}/target/app</outputDirectory>
                  <resources>
                    <resource>
                      <directory>src/jpackage/app</directory>
                      <filtering>true</filtering>
                    </resource>
                  </resources>
                </configuration>
              </execution>
            </executions>
          </plugin>
          <plugin>
            <groupId>com.github.akman</groupId>
            <artifactId>jpackage-maven-plugin</artifactId>
            <version>0.1.5</version>
            <executions>
              <execution>
                <phase>package</phase>
                <goals>
                  <goal>jpackage</goal>
                </goals>
                <configuration>
                  <resourcedir>${project.basedir}/src/jpackage/resources</resourcedir>
                  <input>${project.build.directory}/app</input>
                  <mainjar>lib/${project.artifactId}-${project.version}.jar</mainjar>
                  <mainclass>com.exoreaction.xorcery.examples.greeter.Main</mainclass>


                  <name>greeter</name>
                  <appversion>${project.version}</appversion>
                  <copyright>Copyright eXOReaction AS</copyright>
                  <description>Description</description>
                  <vendor>eXOReaction AS</vendor>
                  <installdir>/opt/exoreaction</installdir>
                  <javaoptions>-Dfile.encoding=UTF-8 -Xms256m -Xmx512m</javaoptions>
                  <dest>${project.build.directory}/jpackage</dest>
                </configuration>
              </execution>
            </executions>
          </plugin>
        </plugins>
      </build>
    </profile>
  </profiles>

En GreeterResourceTest nosotros usamos la extensión Xorcery para la configuración del ambiente de pruebas que va a usar Xorcery vía @RegisterExtension.

class GreeterResourceTest {
  @RegisterExtension
  static XorceryExtension xorceryExtension = XorceryExtension.xorcery()
      .configuration(ConfigurationBuilder::addTestDefaults)
      .addYaml(String.format("""
                    jetty.server.http.enabled: false
                    jetty.server.ssl.port: %d
                    """, Sockets.nextFreePort()))
      .build();


Nosotros testeamos primero el GET del API Rest instanciando un cliente Xorcery.

  @Test
  void updateGreeting() throws Exception {
    Configuration configuration = xorceryExtension.getServiceLocator().getService(Configuration.class);
    URI baseUri = InstanceConfiguration.get(configuration).getURI();
    Client client = xorceryExtension.getServiceLocator().getService(ClientBuilder.class).build();
    {
      String content = client.target(baseUri)
          .path("/api/greeter")
          .request()
          .get(String.class);
      System.out.println(content);
    }



Esta es una configuración donde indicamos que deseamos generar un certificado SSL para el  hostname local,  los recursos API REST API, y la configuración log4j2. Esta configuración es localizada en resources/xorcery.yaml

instance.name: "greeter"
instance.home: "{{ SYSTEM.jpackage_app-path ? jpackage.app | SYSTEM.user_dir}}"
jpackage.app: "{{ SYSTEM.jpackage_app-path }}/../../lib/app"
# So that we can generate a SSL certificate for the local hostname. Replace with whatever domain name you actually use
instance.domain: local
# Add local convenience names for your own computer into the SSL cert
certificates:
  dnsNames:
    - localhost
    - "{{ instance.host }}"
  ipAddresses:
    - 127.0.0.1
    - "{{ instance.ip }}"
# REST API resources
jersey.server.register:
  - com.exoreaction.xorcery.examples.greeter.resources.api.GreeterResource
keystores:
  enabled: true
  keystore:
    path: "{{ home }}/keystore.p12"
    password: "password"
    template: "META-INF/intermediatecakeystore.p12"
log4jpublisher.enabled: false
log4jsubscriber.enabled: false
domainevents.subscriber.configuration.projection: "greeter"
jsondomaineventprojection.enabled: false
log4j2:
  Configuration: ...


El servicio GreeterApplication permite realizar consultas y eventos de dominio que son generados y proyectados a la base de datos (en nuestro ejemplo neo4j).

@Service
@Named(GreeterApplication.SERVICE_TYPE)
public class GreeterApplication {
  public static final String SERVICE_TYPE = "greeter";
  private final DomainEventPublisher domainEventPublisher;
  private final DomainEventMetadata domainEventMetadata;
  private final GraphDatabase graphDatabase;
  @Inject
  public GreeterApplication(DomainEventPublisher domainEventPublisher,
      GraphDatabase graphDatabase) {
    this.domainEventPublisher = domainEventPublisher;
    this.graphDatabase = graphDatabase;
    this.domainEventMetadata = new DomainEventMetadata(new Metadata.Builder()
        .add("domain", "greeter")
        .build());
  }
    // Reads
  public CompletionStage<String> get(String name) {
    return graphDatabase.execute("MATCH (greeter:Greeter {id:$id}) RETURN greeter.greeting as greeting",
            Map.ofEntries(entry("id", "greeter")), 30)
        .thenApply(r ->
        {
          try (GraphResult result = r) {
            return result.getResult().stream().findFirst().map(m -> m.get("greeting").toString()).orElse("Hello World");
          } catch (Exception e) {
            throw new CompletionException(e);
          }
        });
  }

En GreeterResource veremos un ciclo completo de como crear eventos de dominio, publicarlos, luego suscribirnos con el servicio  Neo4jDomainEventsService y proyectarlos a la base de datos, lo cual puede luego ser consultado, a través de, el wrapper GraphDatabase.

Consulta

@Path("api/greeter")
public class GreeterResource
        extends JsonApiResource {
  private GreeterApplication application;
  @Inject
  public GreeterResource(GreeterApplication application) {
    this.application = application;
  }
  @GET
  public CompletionStage<Context> get() {
    return application.get("greeting").handle((g, t)->
    {
      if (t != null)
      {
        LogManager.getLogger(getClass()).error("Could not get greeting", t);
        return "";
      } else
      {
        return g;
      }
    }).thenApply(Context::newContext);
  }

Si nosotros ejecutamos las pruebas, veremos que la llamada a /api/greeter retorna el contenido del template del API en formato html:



Resultados de las pruebas:



Escritura

Nosotros ahora probaremos publicar un evento de dominio y proyectar este a neo4j.


  // Writes
  public CompletionStage<Metadata> handle(Record command) {
    Metadata.Builder metadata = new DomainEventMetadata.Builder(new Metadata.Builder().add(domainEventMetadata.context()))
        .timestamp(System.currentTimeMillis()).builder();
    try {
            List<DomainEvent> events = (List<DomainEvent>) getClass().getDeclaredMethod("handle", command.getClass()).invoke(this, command);
            Metadata md = metadata.add(Model.Metadata.commandName, command.getClass().getName()).build();
            CommandEvents commandEvents = new CommandEvents(md, events);
            return domainEventPublisher.publish(commandEvents);
    } catch (Throwable e) {
      return CompletableFuture.failedStage(e);
    }
  }
    private List<DomainEvent> handle(UpdateGreeting updateGreeting) {
        return Collections.singletonList(JsonDomainEvent.event("UpdatedGreeting").updated("Greeter", "greeter")
                .attribute("greeting", updateGreeting.newGreeting())
                .build());
  }

 

Agregamos el evento de dominio y comando correspondientes:

public record UpdateGreeting(String newGreeting) {

}


Finalmente, si deseamos ver el sandbox HTML generado gracias al JSON:API y JSONSchema,tenemos que agregar el Main.java para usar el runner.

public class Main {
  public static void main(String[] args) {
    com.exoreaction.xorcery.runner.Main.main(args);
  }
}

2023-11-13 12:24:18,506 [RunLevelControllerThread-1699896252353] INFO   c.e.x.j.s.JettyLifecycleService: Started Jetty server

2023-11-13 12:24:18,507 [RunLevelControllerThread-1699896252353] DEBUG  c.e.x.c.RunLevelLogger: Reached run level 18

2023-11-13 12:24:18,507 [RunLevelControllerThread-1699896253228] DEBUG  c.e.x.c.RunLevelLogger: Reached run level 19

2023-11-13 12:24:18,508 [RunLevelControllerThread-1699896253228] DEBUG  c.e.x.c.RunLevelLogger: Reached run level 20

2023-11-13 12:24:18,509 [main] DEBUG macbook-pro-de-jose.local.genericserver c.e.x.c.Xorcery: Services:


Nota: Observe cómo xorcery ejecuta los @RunLevel cuando inicia el proyecto:

 * 0: Configuration refresh
 * 2: Certificate refresh
 * 4: Servers
 * 6: Server publishers/subscribers
 * 8: Client publishers/subscribers
 * 20: DNS registration

Este es el sandbox HTML generado:




Si deseamos publicar el evento de dominio y proyectar este a neo4j, podemos probar con este URL http://localhost/api/greeter 


En futuros posts, vamos a continuar examinando los ejemplos desarrollados con Xorcery.

Enjoy!

Jose







Share:

Xorcery Samples - Greeter

 


We are going to explain our first example with Xorcery called Greeter, which you will find in the following Github repository.

With Xorcery we can implement the API of service as a REST API (using JSON-API as a content type) for request/response needs or reactive stream web sockets (server publishers or subscribers) for streaming needs (like event sourcing or projections or log collection, etc.).

Important fact: The entire implementation of Xorcery uses the Jakarta EE APIs and important libraries in the Java world. Xorcery for example uses HK2 a lightweight and dynamic injection framework.


Why JSON:API




The election in JSON-API and JSONSchema is that then makes it possible to create a REST client, and not an HTTP client, which applications can use. 

When done properly it leads to the situation that all application code interactions with REST clients are one of these two: follow links and submit forms. That's it. There's no constructing URLs, or figuring out what method to use, or any of that. This is all happening behind the scenes as it is defined by JSON: API and JSONSchema on the server. Application code just needs to bother with what link rel's it cares about, what information it needs to extract from resources in JSON: API, and how to submit forms (which know the URI and method to use).

We currently support URI templates in the JSON:API schemas and HTML sandbox. This way it is easier to test the API in a browser, as well as simplify filling out the URLs as a form.

Free HTML Sandbox stuff 

Why Jetty Websockets + Disruptor


Xorcery makes a custom variation of the ReactiveStreams API with WebSockets to send events as headers plus bytes, as well as an integration with the Disruptor API. In the xorcery source code you will find services where the architecture is applied such as publisher+subscriber, metrics events publisher+subscriber, and domain event publisher+subscriber (subscriber maps into hashmap “database”).


Greeter

In the GitHub repository, you will find xorcery-examples where a modular project has been created.

Here you will find the BOM, common dependencies, and plugins.


  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.release>17</maven.compiler.release>

    <xorcery.version>0.79.2</xorcery.version>
    <hk2.version>3.0.5</hk2.version>
    <jersey.version>3.1.3</jersey.version>
    <slf4j.version>2.0.7</slf4j.version>
    <log4j.version>2.22.0</log4j.version>

    <junit.version>5.10.0</junit.version>
    <junit.platform.version>1.9.0</junit.platform.version>
  </properties>


  <modules>
    <module>xorcery-examples-greeter</module>
  </modules>


   <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.exoreaction.xorcery</groupId>
        <artifactId>xorcery-bom</artifactId>
        <version>${xorcery.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-bom</artifactId>
        <version>${log4j.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>org.hamcrest</groupId>
        <artifactId>hamcrest</artifactId>
        <version>2.2</version>
        <scope>test</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

 

The plugins:

  <build>
    <pluginManagement>
      <plugins>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.11.0</version>
          <configuration>
            <annotationProcessorPaths>
              <path>
                <groupId>org.glassfish.hk2</groupId>
                <artifactId>hk2-metadata-generator</artifactId>
                <version>${hk2.version}</version>
              </path>
            </annotationProcessorPaths>
          </configuration>
        </plugin>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-dependency-plugin</artifactId>
          <version>3.6.1</version>
        </plugin>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>3.2.2</version>
          <dependencies>
            <dependency>
              <groupId>me.fabriciorby</groupId>
              <artifactId>maven-surefire-junit5-tree-reporter</artifactId>
              <version>1.2.1</version>
            </dependency>
          </dependencies>
          <configuration>
            <reportFormat>plain</reportFormat>
            <consoleOutputReporter>
              <disable>true</disable>
            </consoleOutputReporter>
            <statelessTestsetInfoReporter
              implementation="org.apache.maven.plugin.surefire.extensions.junit5.JUnit5StatelessTestsetInfoTreeReporter"/>
          </configuration>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>

In order to download Xorcery dependencies, it is mandatory to add the repositories:

  <repositories>
    <repository>
      <id>cantara-releases</id>
      <name>Cantara Release Repository</name>
      <url>https://mvnrepo.cantara.no/content/repositories/releases/</url>
    </repository>
    <repository>
      <id>cantara-snapshots</id>
      <name>Cantara Snapshot Repository</name>
      <url>https://mvnrepo.cantara.no/content/repositories/snapshots/</url>
    </repository>
  </repositories>

  <distributionManagement>
    <repository>
      <id>cantara</id>
      <name>Cantara Release Repository</name>
      <url>https://mvnrepo.cantara.no/content/repositories/releases/</url>
    </repository>
    <snapshotRepository>
      <id>cantara</id>
      <name>Cantara Snapshot Repository</name>
      <url>https://mvnrepo.cantara.no/content/repositories/snapshots/</url>
    </snapshotRepository>
  </distributionManagement>


The test report is presented in a fancy tree output thanks to https://github.com/fabriciorby/maven-surefire-junit5-tree-reporter


Greeter Implementation

This project has xorcery-examples as its parent project.

  <parent>
    <groupId>com.exoreaction.xorcery.examples</groupId>
    <artifactId>xorcery-examples</artifactId>
    <version>1.0-SNAPSHOT</version>
  </parent>


Then we add the core dependencies:


        <!-- Core dependencies -->
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-core</artifactId>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-runner</artifactId>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-metadata</artifactId>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-configuration-api</artifactId>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-json</artifactId>
    </dependency>

The dependencies for REST API:

    <!-- REST API -->
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-jsonapi-jaxrs</artifactId>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-jersey-server</artifactId>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-handlebars</artifactId>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-status-server</artifactId>
    </dependency>

 

Xorcery works with Jetty and Jersey as a Jakarta JAX-RS implementation.


The dependencies for service integration and implementation of reactive streams:

<!-- Integration -->
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-jersey-client</artifactId>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-reactivestreams-client</artifactId>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-reactivestreams-server</artifactId>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-neo4j</artifactId>
      <scope>compile</scope>
      <exclusions>
        <exclusion>
          <groupId>org.glassfish.jersey.containers</groupId>
          <artifactId>jersey-container-servlet</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-domainevents-neo4jprojection</artifactId>
    </dependency>

 Dependencies for Logging:

    <!-- Logging -->
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-log4j</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-core</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-slf4j2-impl</artifactId>
    </dependency>

 Integration with Junit:

    <!-- Test -->
    <dependency>
      <groupId>com.exoreaction.xorcery</groupId>
      <artifactId>xorcery-junit</artifactId>
      <scope>test</scope>
    </dependency>

Finally, we add the profile to use jpackage introduced in Java 14 and create the image of an application and install it.

<profiles>
    <profile>
      <id>jpackage</id>
      <activation>
        <os>
          <name>linux</name>
          <arch>amd64</arch>
        </os>
      </activation>
      <build>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-dependency-plugin</artifactId>
            <version>3.6.1</version>
                        <executions>
              <execution>
                <id>copy-dependencies</id>
                <phase>package</phase>
                <goals>
                  <goal>copy-dependencies</goal>
                </goals>
                <configuration>
                  <includeScope>compile</includeScope>
                  <outputDirectory>${project.build.directory}/app/lib</outputDirectory>
                  <overWriteReleases>false</overWriteReleases>
                  <overWriteSnapshots>true</overWriteSnapshots>
                  <overWriteIfNewer>true</overWriteIfNewer>
                </configuration>
              </execution>
            </executions>
          </plugin>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-antrun-plugin</artifactId>
            <version>3.1.0</version>
                        <executions>
              <execution>
                <id>copy-modularized-jar</id>
                <phase>package</phase>
                <goals>
                  <goal>run</goal>
                </goals>
                <configuration>
                  <target>
                    <copy file="${project.build.directory}/${project.build.finalName}.jar"
                      tofile="${project.build.directory}/app/lib/${project.build.finalName}.jar"
                      overwrite="true"/>
                  </target>
                </configuration>
              </execution>
            </executions>
          </plugin>
          <plugin>
            <artifactId>maven-resources-plugin</artifactId>
            <version>3.3.1</version>
            <executions>
              <execution>
                <id>copy-app-resources</id>
                <phase>package</phase>
                <goals>
                  <goal>copy-resources</goal>
                </goals>
                <configuration>
                  <outputDirectory>${basedir}/target/app</outputDirectory>
                  <resources>
                    <resource>
                      <directory>src/jpackage/app</directory>
                      <filtering>true</filtering>
                    </resource>
                  </resources>
                </configuration>
              </execution>
            </executions>
          </plugin>
          <plugin>
            <groupId>com.github.akman</groupId>
            <artifactId>jpackage-maven-plugin</artifactId>
            <version>0.1.5</version>
            <executions>
              <execution>
                <phase>package</phase>
                <goals>
                  <goal>jpackage</goal>
                </goals>
                <configuration>
                  <resourcedir>${project.basedir}/src/jpackage/resources</resourcedir>
                  <input>${project.build.directory}/app</input>
                  <mainjar>lib/${project.artifactId}-${project.version}.jar</mainjar>
                  <mainclass>com.exoreaction.xorcery.examples.greeter.Main</mainclass>


                  <name>greeter</name>
                  <appversion>${project.version}</appversion>
                  <copyright>Copyright eXOReaction AS</copyright>
                  <description>Description</description>
                  <vendor>eXOReaction AS</vendor>
                  <installdir>/opt/exoreaction</installdir>
                  <javaoptions>-Dfile.encoding=UTF-8 -Xms256m -Xmx512m</javaoptions>
                  <dest>${project.build.directory}/jpackage</dest>
                </configuration>
              </execution>
            </executions>
          </plugin>
        </plugins>
      </build>
    </profile>
  </profiles>

At GreeterResourceTest we use the Xorcery extension for the configuration that tests that use Xorcery need via @RegisterExtension:

class GreeterResourceTest {
  @RegisterExtension
  static XorceryExtension xorceryExtension = XorceryExtension.xorcery()
      .configuration(ConfigurationBuilder::addTestDefaults)
      .addYaml(String.format("""
                    jetty.server.http.enabled: false
                    jetty.server.ssl.port: %d
                    """, Sockets.nextFreePort()))
      .build();

We tested the GET first of a Rest API by instantiating an Xorcery client.

  @Test
  void updateGreeting() throws Exception {
    Configuration configuration = xorceryExtension.getServiceLocator().getService(Configuration.class);
    URI baseUri = InstanceConfiguration.get(configuration).getURI();
    Client client = xorceryExtension.getServiceLocator().getService(ClientBuilder.class).build();
    {
      String content = client.target(baseUri)
          .path("/api/greeter")
          .request()
          .get(String.class);
      System.out.println(content);
    }

This is a configuration where we indicate that we want to generate an SSL certificate for the local hostname, the REST API resources, and the log4j2 configuration are declared. Its configuration is located in resources/xorcery.yaml.

instance.name: "greeter"
instance.home: "{{ SYSTEM.jpackage_app-path ? jpackage.app | SYSTEM.user_dir}}"
jpackage.app: "{{ SYSTEM.jpackage_app-path }}/../../lib/app"
# So that we can generate a SSL certificate for the local hostname. Replace with whatever domain name you actually use
instance.domain: local
# Add local convenience names for your own computer into the SSL cert
certificates:
  dnsNames:
    - localhost
    - "{{ instance.host }}"
  ipAddresses:
    - 127.0.0.1
    - "{{ instance.ip }}"
# REST API resources
jersey.server.register:
  - com.exoreaction.xorcery.examples.greeter.resources.api.GreeterResource
keystores:
  enabled: true
  keystore:
    path: "{{ home }}/keystore.p12"
    password: "password"
    template: "META-INF/intermediatecakeystore.p12"
log4jpublisher.enabled: false
log4jsubscriber.enabled: false
domainevents.subscriber.configuration.projection: "greeter"
jsondomaineventprojection.enabled: false
log4j2:
  Configuration: ...

The service GreeterApplication permits queries and domain events that are generated and projected into the database (in our example neo4j). 

@Service
@Named(GreeterApplication.SERVICE_TYPE)
public class GreeterApplication {
  public static final String SERVICE_TYPE = "greeter";
  private final DomainEventPublisher domainEventPublisher;
  private final DomainEventMetadata domainEventMetadata;
  private final GraphDatabase graphDatabase;
  @Inject
  public GreeterApplication(DomainEventPublisher domainEventPublisher,
      GraphDatabase graphDatabase) {
    this.domainEventPublisher = domainEventPublisher;
    this.graphDatabase = graphDatabase;
    this.domainEventMetadata = new DomainEventMetadata(new Metadata.Builder()
        .add("domain", "greeter")
        .build());
  }
    // Reads
  public CompletionStage<String> get(String name) {
    return graphDatabase.execute("MATCH (greeter:Greeter {id:$id}) RETURN greeter.greeting as greeting",
            Map.ofEntries(entry("id", "greeter")), 30)
        .thenApply(r ->
        {
          try (GraphResult result = r) {
            return result.getResult().stream().findFirst().map(m -> m.get("greeting").toString()).orElse("Hello World");
          } catch (Exception e) {
            throw new CompletionException(e);
          }
        });
  }

In GreeterResource see a full cycle of creating domain events, publishing, then subscribing with Neo4jDomainEventsService and projecting into the database, which then can be read through the GraphDatabase wrapper.


Reads

@Path("api/greeter")
public class GreeterResource
        extends JsonApiResource {
  private GreeterApplication application;
  @Inject
  public GreeterResource(GreeterApplication application) {
    this.application = application;
  }
  @GET
  public CompletionStage<Context> get() {
    return application.get("greeting").handle((g, t)->
    {
      if (t != null)
      {
        LogManager.getLogger(getClass()).error("Could not get greeting", t);
        return "";
      } else
      {
        return g;
      }
    }).thenApply(Context::newContext);
  }

If we run the test we will see that the call to /api/greeter returns the content of the API HTML template:



Tests result:


Writes

We now test publishing a domain event and projection to neo4j.

  // Writes
  public CompletionStage<Metadata> handle(Record command) {
    Metadata.Builder metadata = new DomainEventMetadata.Builder(new Metadata.Builder().add(domainEventMetadata.context()))
        .timestamp(System.currentTimeMillis()).builder();
    try {
            List<DomainEvent> events = (List<DomainEvent>) getClass().getDeclaredMethod("handle", command.getClass()).invoke(this, command);
            Metadata md = metadata.add(Model.Metadata.commandName, command.getClass().getName()).build();
            CommandEvents commandEvents = new CommandEvents(md, events);
            return domainEventPublisher.publish(commandEvents);
    } catch (Throwable e) {
      return CompletableFuture.failedStage(e);
    }
  }
    private List<DomainEvent> handle(UpdateGreeting updateGreeting) {
        return Collections.singletonList(JsonDomainEvent.event("UpdatedGreeting").updated("Greeter", "greeter")
                .attribute("greeting", updateGreeting.newGreeting())
                .build());
  }

We add the command:

public record UpdateGreeting(String newGreeting) {

}

Finally, if we want to see the HTML sandbox generated thanks to JSON:API and JSONSchema, we have to add the Main.java to use the runner.


public class Main {
  public static void main(String[] args) {
    com.exoreaction.xorcery.runner.Main.main(args);
  }
}


2023-11-13 12:24:18,506 [RunLevelControllerThread-1699896252353] INFO   c.e.x.j.s.JettyLifecycleService: Started Jetty server

2023-11-13 12:24:18,507 [RunLevelControllerThread-1699896252353] DEBUG  c.e.x.c.RunLevelLogger: Reached run level 18

2023-11-13 12:24:18,507 [RunLevelControllerThread-1699896253228] DEBUG  c.e.x.c.RunLevelLogger: Reached run level 19

2023-11-13 12:24:18,508 [RunLevelControllerThread-1699896253228] DEBUG  c.e.x.c.RunLevelLogger: Reached run level 20

2023-11-13 12:24:18,509 [main] DEBUG macbook-pro-de-jose.local.genericserver c.e.x.c.Xorcery: Services:


Note: Observe how xorcery executes the level settings when launching the project:

 * 0: Configuration refresh
 * 2: Certificate refresh
 * 4: Servers
 * 6: Server publishers/subscribers
 * 8: Client publishers/subscribers
 * 20: DNS registration

The HTML sandbox:



If we want to publish the domain event and project it to neo4j we can try this URL http://localhost/api/greeter 


In subsequent posts, we will continue examining more examples of using Xorcery.


Enjoy!


Jose


Share: