LangChain for Java in Quadim

 


LangChain for Java: Supercharge your Java application with the power of LLMs


At Quadim we have worked with langchain4j. The goal of this project is to simplify the integration of AI/LLM capabilities into your Java application.

Below we are going to show you some simple examples of using this library so that you can use it in your projects.


Prerequisite


You need an OpenAI API Key. In this link you will find detailed information to obtain your API Key.

In the source code, you will find the ApiKeys class, which is where you will use your API KEY.

public class ApiKeys {

  public static final String MY_OPENAI_API_KEY ="<YOU MUST HERE PUT YOUR API KEY>";

}

Assistant Mode

In the AiAssistedHRAssistantTest we are going to use a prompt that will answer as if it were a virtual HR assistant in Quadim.

Below I will explain the source code in parts:

  @Test
  public void testChatWithHRAI() throws Exception {
    Random r = new Random();
    int userNo = r.nextInt(100);
    // Plan
    ChatLanguageModel model = OpenAiChatModel.builder()
        .apiKey(ApiKeys.MY_OPENAI_API_KEY)
        .modelName(OpenAiModelName.GPT_3_5_TURBO)
        .timeout(ofSeconds(900))
        .temperature(0.9)
        .build();

  1. OpenAiChatModel.builder(): This creates a new builder for the OpenAiChatModel class.
  2. .apiKey(ApiKeys.MY_OPENAI_API_KEY): Sets the API key for the OpenAI model. You need to replace ApiKeys.MY_OPENAI_API_KEY with your actual OpenAI API key.
  3. .modelName(OpenAiModelName.GPT_3_5_TURBO): Sets the model name to GPT-3.5 Turbo. This specifies the version of the OpenAI language model you want to use.
  4. .timeout(ofSeconds(900)): Sets the timeout for the API call to 900 seconds (15 minutes). This means that if the API call takes longer than 15 minutes, it will be aborted.
  5. .temperature(0.9): Sets the temperature parameter for sampling. A higher temperature (e.g., 0.9) makes the output more random, while a lower temperature (e.g., 0.2) makes the output more focused and deterministic.
  6. .build(): Builds the ChatLanguageModel instance with the specified configurations.

After executing this code, you will have a ChatLanguageModel instance named model configured with the specified parameters. You can then use this model to generate language-based responses using the LangChain library in Java. 

Assistant assistant = AiServices.builder(Assistant.class)
        .chatLanguageModel(model)
        .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
        .build();
  1. AiServices.builder(Assistant.class): This creates a builder for the Assistant class within the AiServices utility.
  2. .chatLanguageModel(model): Sets the ChatLanguageModel for the assistant. The model here is the ChatLanguageModel instance that you created in the previous code block.
  3. .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10)): Sets the chat memory provider for the assistant. It uses a lambda expression to create a MessageWindowChatMemory with a maximum of 10 messages. This means that the assistant will keep track of the conversation history, and in this case, it will retain the last 10 messages.
  4. .build(): Builds the Assistant instance with the specified configurations.
After executing this code, you will have an Assistant instance named assistant configured with the specified language model (model) and chat memory provider. This Assistant can then be used to interact with the language model and manage conversation history.


// a) create types for retrieving skills objects from responses
SkillExtractor skillExtractor = AiServices.create(SkillExtractor.class, model);
static class SkillReference {

    @Description("the name of this skill")
    private String name;
    @Description("description of this skill. please make it selling and not more than 10 lines of text")
    private String description;

    //...
}

  1. AiServices.create(SkillExtractor.class, model): This creates an instance of the SkillExtractor class using the AiServices utility. It takes the SkillExtractor class as a parameter and the model (presumably the ChatLanguageModel instance) as another parameter.
  2. static class SkillReference: This declares a static nested class named SkillReference. This class has two fields (name and description) with corresponding @Description annotations. These annotations might be used for documentation or metadata purposes.
The SkillExtractor instance (skillExtractor) is likely to be used for extracting skills or features from the language model (model). The SkillReference class appears to be a structure for holding information about a skill, with name and description attributes.


    // b) simulate a chat
    String appendPrompt = "Answer acting as a friendly HR Consultant helping the user with his/her competence mapping, focussing on skills and projects."+
        "Structure the answer friendly and selling with bullets for discovered or suggested supporting skills and potential typical projects"+
        "where the user may have used those skills. " +
        "Limit answer to the most relevant 5 skills and top 8 projects";


    String q1 = "Yes, I do work with Java and java microservices on the backend ";
    System.out.println("me: " + q1);
    String res1 = assistant.chat(userNo, q1 + appendPrompt);
    System.out.println(res1);
    Skill extractedSkills1 = skillExtractor.extractSkillFrom(res1);
    System.out.println("\n\n1. Skill mapped:" + mapper.writerWithDefaultPrettyPrinter().writeValueAsString(extractedSkills1) + "\n\n");


The assistant is defined this way:


  interface Assistant {
    String chat(@MemoryId int memoryId, @UserMessage String userMessage);
  }
 

So if we run the test, the AI assistant will tell us what skills we need to have to develop with Java microservices.

Result:



Below I show you the test log:

me: Yes, I do work with Java and java microservices on the backend 
Skills:
- Java programming: Experience in Java programming is essential for working with Java microservices on the backend. This includes a deep understanding of object-oriented programming concepts, data structures, and algorithms.
- Spring Framework: Knowledge of the Spring Framework is vital for developing Java microservices. This includes proficiency in Spring Boot, Spring Data, and Spring Cloud.
- RESTful API development: Understanding how to design and develop RESTful APIs is necessary for creating microservices. This involves knowledge of HTTP, JSON, and API documentation tools like Swagger.
- Containerization and orchestration: Proficiency in containerization technologies like Docker and container orchestration platforms like Kubernetes is crucial for scaling and managing microservices in a distributed environment.
- Database management: Having experience with relational databases like MySQL or PostgreSQL, as well as NoSQL databases like MongoDB or Redis, is important for storing and retrieving data in microservices.

Projects:
1. Building a microservices-based e-commerce platform: Developing a scalable and fault-tolerant e-commerce platform using Java microservices, Spring Boot, and containerization technologies like Docker. Implementing RESTful APIs for product catalog management, order processing, and payment integration.

2. Creating a social media analytics system: Designing a system to analyze and process large volumes of social media data using Java microservices, Spring Cloud, and Apache Kafka for stream processing. Implementing sentiment analysis, trend detection, and user engagement metrics.

3. Developing a microservices-based banking application: Building a secure and highly available banking application using Java microservices, Spring Boot, and container orchestration with Kubernetes. Creating APIs for account management, transaction processing, and fraud detection.

4. Building a document management system: Creating a system to store and manage documents using Java microservices, Spring Data, and Elasticsearch for full-text search capabilities. Implementing features like document versioning, access control, and document tagging.

5. Designing a real-time chat application: Developing a real-time messaging application using Java microservices, Spring Boot, and WebSocket technology. Implementing features like chat rooms, private messaging, and message history.

6. Building a microservices-based healthcare platform: Designing a platform for managing patient records, appointments, and healthcare providers using Java microservices, Spring Cloud, and a combination of relational and NoSQL databases. Implementing secure authentication, data encryption, and integration with external healthcare systems.

7. Creating a recommendation system: Developing a recommendation engine using Java microservices, Spring Boot, and machine learning algorithms. Implementing personalized recommendations based on user preferences, purchase history, and browsing behavior.

8. Building a microservices-based travel booking system: Designing a system for managing travel bookings, flight reservations, and hotel accommodations using Java microservices, Spring Cloud, and messaging queues like RabbitMQ. Implementing features like real-time availability updates, payment processing, and itinerary generation.

Note: These suggested projects are not exhaustive but provide a range of examples where the mentioned skills could be utilized.


1. Skill mapped:{
  "name" : "Java programming",
  "description" : "Java programming is a crucial skill for working with Java microservices on the backend. With a deep understanding of object-oriented programming concepts, data structures, and algorithms, you'll be able to develop robust and efficient microservices. Java's versatility and extensive libraries make it a popular choice among developers worldwide, ensuring ample resources and community support.",
  "listOfCandidateSkillDefinitions" : [ {
    "name" : null,
    "description" : null
  }, {
    "name" : null,
    "description" : null
  }, {
    "name" : null,
    "description" : null
  } ]
}


Translator Mode

In the AiAssistedTranslationTest we will see how to test the language translation of our virtual assistant.



 @Test
    public void testAIAssistedTranslationFromEnglishToNorwegian() throws Exception {


        // PLan
        ChatLanguageModel model = OpenAiChatModel.builder()
            .apiKey(ApiKeys.MY_OPENAI_API_KEY)
            .modelName(OpenAiModelName.GPT_3_5_TURBO_16K)
            .timeout(ofSeconds(900))
            .temperature(0.2)
            .build();


        String initialProjectDescription = "Chief Developer, Technical Architect: Developed several core modules in PasientSky's platform, " +
            "including communication module against Norwegian public health authorities, topology module for clinical diagnosis " +
            "(ICPC2, CDC10 and SNOWMED), product module (medicines and prescription goods) m.m. Technical architect, Quality assurer. ";



        int n = 343;

        try {

            String res0 = model.generate( "Translate " + initialProjectDescription + " from English to Norwegian");
            System.out.println(n + " Translated descriptions:" + n++ + "\n\n" + res0 + "\n\n");
        } catch (Exception e) {
            System.out.println("Exception handling  - Stacktrace:" + Arrays.toString(e.getStackTrace()));
        }
    }

In this example, we will see how it can translate from English to Norwegian. The result is:


Below I show you the test log:

Hovedutvikler, Teknisk arkitekt: Utviklet flere kjernekomponenter i PasientSky-plattformen, inkludert kommunikasjonsmodul mot norske helsemyndigheter, topologimodul for klinisk diagnose (ICPC2, CDC10 og SNOWMED), produktmodul (medisiner og reseptvarer) m.m. Teknisk arkitekt, Kvalitetssikrer.


Parse PDF Resume and get skills 


In this last example we search a directory for CVs in PDF format, we ask you to extract the skills in a summarized format and then compare it with a skills base found in skilldefinitions.json to see which skills you have and which you don't.

@Test
  public void testParseOSDResumeWithAIProducingSkillDefinitions() throws Exception {
    helper = new SkillDefinitionHelper();

    Map<String, List<SimplifiedSkill>> resultMap = new HashMap<>();
    // PLan
    ChatLanguageModel model = OpenAiChatModel.builder()
        .apiKey(ApiKeys.MY_OPENAI_API_KEY)
        .modelName(OpenAiModelName.GPT_4)
        .timeout(ofSeconds(900))
        .temperature(0.6)
        .build();
    Assistant assistant = AiServices.builder(Assistant.class)
        .chatLanguageModel(model)
        .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
        .build();


    // a) Get list of PDF Resumes
    List<Resource> resourceList = getPDFResources();


    String appendPrompt = "Extract the users skills from this resume. Present the result as structured " +
        "json data in the following json format " + jsonSkillDefinition + " keep the name of the skill short";

    int n = 0;
    boolean RUN_FULL_REGRESSON = true;
    int found_and_swapped = 0;
    int not_found = 0;
    if (RUN_FULL_REGRESSON) {
      for (Resource resource : resourceList) {
        try {
          File file = resource.getFile();
          PDDocument document = Loader.loadPDF(file);
          PDFTextStripper stripper = new PDFTextStripper();
          String text = stripper.getText(document);
          //System.out.println(n + " Input data extracted from pdf resume:\n" + text + "\n\n");
          String res0 = assistant.chat(n, text + appendPrompt);
          //System.out.println(n + " Generated JSON SkillDefinitions:" + n++ + "\n\n" + res0 + "\n\n");
          List<SimplifiedSkill> simplifiedSkills = mapper.readValue(res0, new TypeReference<List<SimplifiedSkill>>() {
          });
          System.out.println("Mapped skill definitions:" + simplifiedSkills.size() + " for pdf:" + resource.getFilename() + "\n\n\n");
          List<SimplifiedSkill> enhancedSkillList = helper.getEnhancedSkillDefinitions(simplifiedSkills);
          resultMap.put(resource.getFilename(), simplifiedSkills);
          resultMap.put(resource.getFilename() + "-enhanced", enhancedSkillList);

        } catch (Exception e) {
          System.out.println("Exception handling " + resource.getFilename() + " - Stacktrace:" + Arrays.toString(e.getStackTrace()));
        }
      }
    }
    System.out.println(resultMap);

  }


As you can see, the virtual assistant returns the information as indicated. Amazing.



The log:

[
  {
    "name": "C language",
    "description": "Proficiency in C language, a popular programming language in software development.",
    "isPublic": false,
    "currentVersion": 1,
    "additionaljsonproperties": null,
    "public": false
  },
  {
    "name": "Javascript",
    "description": "Knowledge in Javascript, a programming language used primarily for web development.",
    "isPublic": false,
    "currentVersion": 1,
    "additionaljsonproperties": null,
    "public": false
  },
  {
    "name": "HTML",
    "description": "Proficiency in HTML, a standard language for designing and creating websites.",
    "isPublic": false,
    "currentVersion": 1,
    "additionaljsonproperties": null,
    "public": false
  },
  {
    "name": "CSS",
    "description": "Knowledge in CSS, a style sheet language used for describing the look and formatting of a document written in HTML.",
    "isPublic": false,
    "currentVersion": 1,
    "additionaljsonproperties": null,
    "public": false
  },
  {
    "name": "Microsoft Excel",
    "description": "Skills in Microsoft Excel, a spreadsheet program used to store and process data.",
    "isPublic": false,
    "currentVersion": 1,
    "additionaljsonproperties": null,
    "public": false
  },
  {
    "name": "Microsoft Word",
    "description": "Proficiency in Microsoft Word, a word processing software used to create, edit, and print documents.",
    "isPublic": false,
    "currentVersion": 1,
    "additionaljsonproperties": null,
    "public": false
  },
  {
    "name": "PowerPoint",
    "description": "Skills in PowerPoint, a presentation program used for creating slideshow presentations.",
    "isPublic": false,
    "currentVersion": 1,
    "additionaljsonproperties": null,
    "public": false
  },
  {
    "name": "Graphic Design",
    "description": "Experience in freelance graphic design, including creating social media, logos, and image editing.",
    "isPublic": false,
    "currentVersion": 1,
    "additionaljsonproperties": null,
    "public": false
  },
  {
    "name": "Customer Service",
    "description": "Experience in customer service roles, including working as a waiter, cashier, and attendant.",
    "isPublic": false,
    "currentVersion": 1,
    "additionaljsonproperties": null,
    "public": false
  }
]

Conclusion

We can integrate this library into our programs and enjoy the power of LLMS. The possibilities are endless.

Enjoy!

Jose

Share:

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: