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:

0 comentarios:

Publicar un comentario