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:

0 comentarios:

Publicar un comentario