drivingAdapterClass, JexxaMain jexxaMain)
23 | {
24 | AdapterConvention.validate(drivingAdapterClass);
25 |
26 | this.drivingAdapterClass = Objects.requireNonNull(drivingAdapterClass);
27 | this.jexxaMain = Objects.requireNonNull(jexxaMain);
28 | this.conditionalBind = Objects.requireNonNull(conditionalBind);
29 | }
30 |
31 | public JexxaMain to(Class
port)
32 | {
33 | Objects.requireNonNull(port);
34 |
35 | if ( !conditionalBind.getAsBoolean())
36 | {
37 | return jexxaMain;
38 | }
39 |
40 | if ( AdapterConvention.isPortAdapter(port, jexxaMain.getInfrastructure()))
41 | {
42 | jexxaMain.bindToPortAdapter(drivingAdapterClass, port);
43 | return jexxaMain;
44 | }
45 |
46 | PortConvention.validate(port);
47 |
48 | jexxaMain.bindToPort(drivingAdapterClass, port);
49 |
50 | return jexxaMain;
51 | }
52 |
53 | public JexxaMain to(Object port)
54 | {
55 | Objects.requireNonNull(port);
56 |
57 | if ( !conditionalBind.getAsBoolean())
58 | {
59 | return jexxaMain;
60 | }
61 |
62 | return jexxaMain.bindToPort(drivingAdapterClass, port);
63 | }
64 |
65 | public
JexxaMain toAnnotation(Class
annotation)
66 | {
67 | Objects.requireNonNull(annotation);
68 |
69 | if ( !conditionalBind.getAsBoolean())
70 | {
71 | return jexxaMain;
72 | }
73 |
74 | jexxaMain.bindToAnnotatedPorts(drivingAdapterClass, annotation);
75 | return jexxaMain;
76 | }
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/jexxa-test/src/main/java/io/jexxa/jexxatest/architecture/ProjectContent.java:
--------------------------------------------------------------------------------
1 | package io.jexxa.jexxatest.architecture;
2 |
3 | import com.tngtech.archunit.base.DescribedPredicate;
4 | import com.tngtech.archunit.core.domain.JavaClass;
5 | import com.tngtech.archunit.core.domain.JavaClasses;
6 | import com.tngtech.archunit.core.importer.ClassFileImporter;
7 | import com.tngtech.archunit.core.importer.ImportOption;
8 |
9 | @SuppressWarnings("unused")
10 | public abstract class ProjectContent
11 | {
12 | private final Class> project;
13 | private JavaClasses importedClasses;
14 |
15 | protected ProjectContent(Class> project, ImportOption importOption)
16 | {
17 | this.project = project;
18 | importedClasses = new ClassFileImporter()
19 | .withImportOption(importOption)
20 | .importPackages(
21 | project.getPackageName()+ ".domain..",
22 | project.getPackageName()+ ".domainservice..",
23 | project.getPackageName()+ ".applicationservice..",
24 | project.getPackageName()+ ".infrastructure.." );
25 | }
26 |
27 | public ProjectContent ignoreClass(Class> clazz)
28 | {
29 | importedClasses = importedClasses.that(isNot(clazz));
30 | return this;
31 | }
32 |
33 | public ProjectContent ignorePackage(String packageName)
34 | {
35 | importedClasses = importedClasses.that(areNotIn(packageName));
36 | return this;
37 | }
38 | public abstract void validate();
39 |
40 | protected JavaClasses importedClasses()
41 | {
42 | return importedClasses;
43 | }
44 |
45 | protected Class> project()
46 | {
47 | return project;
48 | }
49 | private static DescribedPredicate isNot(Class>clazz) {
50 | return new DescribedPredicate<>("Ignore class " + clazz.getSimpleName()) {
51 | @Override
52 | public boolean test(JavaClass javaClass) {
53 | return !javaClass.isEquivalentTo(clazz);
54 | }
55 | };
56 | }
57 |
58 | private static DescribedPredicate areNotIn(String packageName) {
59 | return new DescribedPredicate<>("Ignore package " + packageName) {
60 | @Override
61 | public boolean test(JavaClass javaClass) {
62 | return !javaClass.getPackage().getName().contains(packageName);
63 | }
64 | };
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/jexxa-test/src/main/java/io/jexxa/jexxatest/infrastructure/messaging/recording/MessageRecordingStrategy.java:
--------------------------------------------------------------------------------
1 | package io.jexxa.jexxatest.infrastructure.messaging.recording;
2 |
3 | import io.jexxa.common.facade.utils.annotation.CheckReturnValue;
4 | import io.jexxa.common.drivenadapter.messaging.DestinationType;
5 | import io.jexxa.common.drivenadapter.messaging.MessageBuilder;
6 | import io.jexxa.common.drivenadapter.messaging.MessageSender;
7 |
8 | import java.util.Objects;
9 | import java.util.Properties;
10 |
11 | public class MessageRecordingStrategy extends MessageSender
12 | {
13 | private Object currentMessage;
14 | private MessageRecorder messageRecorder;
15 |
16 | @CheckReturnValue
17 | @Override
18 | public MessageBuilder send(T message)
19 | {
20 | Objects.requireNonNull(message);
21 |
22 | //Get a caller object of this class. Here we assume that it is the implementation of a driven adapter
23 | var walker = StackWalker
24 | .getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);
25 | Class> callerClass = walker.getCallerClass();
26 |
27 | currentMessage = message;
28 | messageRecorder = MessageRecorderManager.getMessageRecorder(callerClass);
29 | return new RecordableMessageBuilder(message, this);
30 | }
31 |
32 | @Override
33 | protected void sendToQueue(String message, String destination, Properties messageProperties, MessageType messageType)
34 | {
35 | messageRecorder.put(new RecordedMessage(
36 | currentMessage,
37 | message,
38 | DestinationType.QUEUE,
39 | destination,
40 | messageProperties,
41 | messageType)
42 | );
43 | }
44 |
45 | @Override
46 | protected void sendToTopic(String message, String destination, Properties messageProperties, MessageType messageType)
47 | {
48 | messageRecorder.put(new RecordedMessage(
49 | currentMessage,
50 | message,
51 | DestinationType.TOPIC,
52 | destination,
53 | messageProperties,
54 | messageType)
55 | );
56 | }
57 |
58 | private static class RecordableMessageBuilder extends MessageBuilder
59 | {
60 | protected RecordableMessageBuilder(T message, MessageRecordingStrategy jmsSender)
61 | {
62 | super(message, jmsSender, MessageType.TEXT_MESSAGE);
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/jexxa-test/src/main/java/io/jexxa/jexxatest/JexxaIntegrationTest.java:
--------------------------------------------------------------------------------
1 | package io.jexxa.jexxatest;
2 |
3 | import io.jexxa.common.facade.logger.SLF4jLogger;
4 | import io.jexxa.core.JexxaMain;
5 | import kong.unirest.Unirest;
6 |
7 | import java.lang.reflect.Constructor;
8 | import java.util.HashMap;
9 | import java.util.Map;
10 | import java.util.Properties;
11 |
12 | import static io.jexxa.jexxatest.JexxaTest.loadJexxaTestProperties;
13 |
14 | public class JexxaIntegrationTest
15 | {
16 | private final Properties properties;
17 | private final Class> application;
18 | private final JexxaMain jexxaMain;
19 | private final Map, AutoCloseable> bindingMap = new HashMap<>();
20 |
21 | public JexxaIntegrationTest(Class> application)
22 | {
23 | this.application = application;
24 | jexxaMain = new JexxaMain(application);
25 | jexxaMain.addProperties( loadJexxaTestProperties() );
26 | this.properties = jexxaMain.getProperties();
27 | }
28 |
29 | public T getBinding(Class bindingClazz)
30 | {
31 | bindingMap.putIfAbsent(bindingClazz, createInstance(bindingClazz, application, properties));
32 | return bindingClazz.cast(bindingMap.get(bindingClazz));
33 | }
34 |
35 | public Properties getProperties() {
36 | return properties;
37 | }
38 |
39 | public void shutDown()
40 | {
41 | bindingMap.values().forEach(this::close);
42 | bindingMap.clear();
43 | Unirest.shutDown();
44 | jexxaMain.stop();
45 | }
46 |
47 | private void close(AutoCloseable autoCloseable)
48 | {
49 | try{
50 | autoCloseable.close();
51 | } catch (Exception e){
52 | SLF4jLogger.getLogger(JexxaIntegrationTest.class).error("Could not close Binding {}", e.getMessage());
53 | }
54 | }
55 |
56 | private static T createInstance(Class clazz, Class> targetClass, Properties props) {
57 | try {
58 | // Suche den Konstruktor (Class, Properties)
59 | Constructor constructor = clazz.getConstructor(Class.class, Properties.class);
60 |
61 | // Erzeuge Instanz
62 | return constructor.newInstance(targetClass, props);
63 |
64 | } catch (NoSuchMethodException e) {
65 | throw new IllegalArgumentException("Class " + clazz.getName() +
66 | " does not provide a constructor of (Class, Properties).", e);
67 | } catch (Exception e) {
68 | throw new IllegalArgumentException("Given properties can not be used to create " + clazz.getName(), e);
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/jexxa-test/src/test/java/io/jexxa/jexxatest/architecture/ArchitectureTest.java:
--------------------------------------------------------------------------------
1 | package io.jexxa.jexxatest.architecture;
2 |
3 | import com.tngtech.archunit.core.importer.ImportOption;
4 | import io.jexxa.jexxatest.architecture.invalidapplication.InvalidApplication;
5 | import io.jexxa.jexxatest.architecture.validapplication.ValidApplication;
6 | import org.junit.jupiter.api.Test;
7 |
8 | import static org.junit.jupiter.api.Assertions.assertThrows;
9 |
10 | class ArchitectureTest {
11 |
12 | @Test
13 | void validateOnionArchitecture()
14 | {
15 | var objectUnderTest = new PortsAndAdapters(ValidApplication.class, ImportOption.Predefined.ONLY_INCLUDE_TESTS)
16 | .addDrivenAdapterPackage("jms")
17 |
18 | .addDrivingAdapterPackage("rest")
19 | .addDrivingAdapterPackage("jms");
20 |
21 | objectUnderTest.validate();
22 | }
23 |
24 | @Test
25 | void validatePatternLanguage()
26 | {
27 | var objectUnderTest = new PatternLanguage(ValidApplication.class, ImportOption.Predefined.ONLY_INCLUDE_TESTS);
28 | objectUnderTest.validate();
29 | }
30 |
31 | @Test
32 | void validateAggregates()
33 | {
34 | var objectUnderTest = new AggregateRules(ValidApplication.class, ImportOption.Predefined.ONLY_INCLUDE_TESTS);
35 | objectUnderTest.validate();
36 | }
37 |
38 | @Test
39 | void validateInvalidOnionArchitecture()
40 | {
41 | var objectUnderTest = new PortsAndAdapters(InvalidApplication.class, ImportOption.Predefined.ONLY_INCLUDE_TESTS)
42 | .addDrivingAdapterPackage("rest");
43 | assertThrows(AssertionError.class, objectUnderTest::validate);
44 | }
45 |
46 | @Test
47 | void validateInvalidPatternLanguage()
48 | {
49 | var objectUnderTest = new PatternLanguage(InvalidApplication.class, ImportOption.Predefined.ONLY_INCLUDE_TESTS);
50 | assertThrows(AssertionError.class, objectUnderTest::validate);
51 | }
52 |
53 | @Test
54 | void validateInvalidAggregates()
55 | {
56 | var objectUnderTest = new AggregateRules(InvalidApplication.class, ImportOption.Predefined.ONLY_INCLUDE_TESTS);
57 |
58 | assertThrows(AssertionError.class, objectUnderTest::validateOnlyAggregatesHaveAggregatesAsFields);
59 | assertThrows(AssertionError.class, objectUnderTest::validateOnlyAggregatesAndNestedClassesAreMutable);
60 | assertThrows(AssertionError.class, objectUnderTest::validateOnlyRepositoriesAcceptAggregates);
61 | assertThrows(AssertionError.class, objectUnderTest::validateReturnAggregates);
62 | assertThrows(AssertionError.class, objectUnderTest::validateAggregateID);
63 | assertThrows(AssertionError.class, objectUnderTest::validate);
64 | }
65 | }
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ master ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ master ]
20 | schedule:
21 | - cron: '0 11 * * 4'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'java' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v6
42 |
43 | - name: Setup Java
44 | uses: actions/setup-java@v5
45 | with:
46 | distribution: 'temurin'
47 | java-version: '25'
48 |
49 | # Initializes the CodeQL tools for scanning.
50 | - name: Initialize CodeQL
51 | uses: github/codeql-action/init@v4
52 | with:
53 | languages: ${{ matrix.language }}
54 | queries: security-and-quality
55 | # If you wish to specify custom queries, you can do so here or in a config file.
56 | # By default, queries listed here will override any specified in a config file.
57 | # Prefix the list here with "+" to use these queries and those in the config file.
58 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
59 |
60 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
61 | # If this step fails, then you should remove it and run the build manually (see below)
62 | - name: Autobuild
63 | uses: github/codeql-action/autobuild@v4
64 |
65 | # ℹ️ Command-line programs to run using the OS shell.
66 | # 📚 https://git.io/JvXDl
67 |
68 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
69 | # and modify them (or add more) to build your code if your project
70 | # uses a compiled language
71 |
72 | #- run: |
73 | # make bootstrap
74 | # make release
75 |
76 | - name: Perform CodeQL Analysis
77 | uses: github/codeql-action/analyze@v4
78 |
--------------------------------------------------------------------------------
/jexxa-core/src/main/java/io/jexxa/core/factory/DependencyScanner.java:
--------------------------------------------------------------------------------
1 | package io.jexxa.core.factory;
2 |
3 | import io.github.classgraph.ClassGraph;
4 | import io.github.classgraph.ScanResult;
5 |
6 | import java.lang.annotation.Annotation;
7 | import java.lang.annotation.Retention;
8 | import java.lang.annotation.RetentionPolicy;
9 | import java.util.ArrayList;
10 | import java.util.List;
11 | import java.util.Objects;
12 |
13 | final class DependencyScanner
14 | {
15 | private final List acceptedPackages = new ArrayList<>();
16 | private ScanResult scanResult;
17 |
18 | DependencyScanner acceptPackage(String packageName)
19 | {
20 | acceptedPackages.add(packageName);
21 | scanResult = null; //Reset scan result so that it is recreated with new accepted packages
22 | return this;
23 | }
24 |
25 | List getAcceptPackages()
26 | {
27 | return acceptedPackages;
28 | }
29 |
30 | List> getClassesWithAnnotation(final Class extends Annotation> annotation)
31 | {
32 | validateRetentionRuntime(annotation);
33 |
34 | return getScanResult()
35 | .getClassesWithAnnotation(annotation.getName())
36 | .loadClasses();
37 | }
38 |
39 |
40 | List> getClassesImplementing(final Class> interfaceType)
41 | {
42 | Objects.requireNonNull(interfaceType);
43 | return getScanResult()
44 | .getClassesImplementing(interfaceType.getName())
45 | .loadClasses();
46 | }
47 |
48 |
49 |
50 | private void validateRetentionRuntime(final Class extends Annotation> annotation) {
51 | Objects.requireNonNull(annotation.getAnnotation(Retention.class), "Annotation must be declared with '@Retention(RUNTIME)'" );
52 | if (!annotation.getAnnotation(Retention.class).value().equals(RetentionPolicy.RUNTIME))
53 | {
54 | throw new IllegalArgumentException("Annotation must be declared with '@Retention(RUNTIME)");
55 | }
56 | }
57 |
58 | private ScanResult getScanResult()
59 | {
60 | if ( scanResult == null )
61 | {
62 | if (acceptedPackages.isEmpty())
63 | {
64 | scanResult = new ClassGraph()
65 | .enableAnnotationInfo()
66 | .enableClassInfo()
67 | .scan();
68 | }
69 | else
70 | {
71 | scanResult = new ClassGraph()
72 | .enableAnnotationInfo()
73 | .enableClassInfo()
74 | .acceptPackages(acceptedPackages.toArray(new String[0]))
75 | .scan();
76 |
77 | }
78 | }
79 |
80 | return scanResult;
81 | }
82 |
83 | }
84 |
--------------------------------------------------------------------------------
/jexxa-core/src/test/java/io/jexxa/testapplication/domain/model/JexxaObject.java:
--------------------------------------------------------------------------------
1 | package io.jexxa.testapplication.domain.model;
2 |
3 | import java.util.Objects;
4 |
5 | import static java.lang.Math.floor;
6 | import static java.lang.Math.log;
7 |
8 | public final class JexxaObject
9 | {
10 | private final JexxaEntity jexxaEntity;
11 | private final JexxaValueObject jexxaValueObject;
12 | private JexxaValueObject optionalJexxaValue;
13 | private String optionalString;
14 | private final String internalString;
15 |
16 | public void setOptionalValue(JexxaValueObject optionalJexxaValue)
17 | {
18 | this.optionalJexxaValue = optionalJexxaValue;
19 | }
20 |
21 | public void setOptionalString(String optionalString)
22 | {
23 | this.optionalString = optionalString;
24 | }
25 |
26 | public String getOptionalString()
27 | {
28 | return optionalString;
29 | }
30 |
31 | public String getString()
32 | {
33 | return internalString;
34 | }
35 |
36 | public JexxaValueObject getOptionalValue()
37 | {
38 | return optionalJexxaValue;
39 | }
40 |
41 | private JexxaObject(JexxaValueObject jexxaValueObject, String internalString)
42 | {
43 | this.jexxaEntity = JexxaEntity.create(jexxaValueObject);
44 | this.jexxaValueObject = jexxaValueObject;
45 | this.internalString = internalString;
46 | this.optionalString = null;
47 | this.optionalJexxaValue = null;
48 | }
49 |
50 | // Create a sequence of chars 'A' .. 'Z', 'AA', ...
51 | public static String createCharSequence(int n) {
52 | var counter = n;
53 | char[] buf = new char[(int) floor(log(25 * (counter + 1)) / log(26))];
54 | for (int i = buf.length - 1; i >= 0; i--)
55 | {
56 | counter--;
57 | buf[i] = (char) ('A' + counter % 26);
58 | counter /= 26;
59 | }
60 | return new String(buf);
61 | }
62 |
63 | public void setInternalValue(int value)
64 | {
65 | jexxaEntity.setInternalValue(value);
66 | }
67 |
68 | public int getInternalValue()
69 | {
70 | return jexxaEntity.getInternalValue();
71 | }
72 |
73 | public JexxaValueObject getKey()
74 | {
75 | return jexxaValueObject;
76 | }
77 |
78 | public static JexxaObject create(JexxaValueObject key)
79 | {
80 | return new JexxaObject(key, createCharSequence(key.getValue()));
81 | }
82 |
83 | @Override
84 | public boolean equals(Object o)
85 | {
86 | if (this == o)
87 | {
88 | return true;
89 | }
90 | if (o == null || getClass() != o.getClass())
91 | {
92 | return false;
93 | }
94 | JexxaObject that = (JexxaObject) o;
95 | return Objects.equals(getKey(), that.getKey()); // Only compare keys
96 | }
97 |
98 | @Override
99 | public int hashCode()
100 | {
101 | return Objects.hash(jexxaValueObject);
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/jexxa-web/src/test/java/io/jexxa/drivingadapter/rest/MultipleRESTClientsIT.java:
--------------------------------------------------------------------------------
1 | package io.jexxa.drivingadapter.rest;
2 |
3 | import io.jexxa.TestConstants;
4 | import io.jexxa.core.JexxaMain;
5 | import io.jexxa.testapplication.JexxaTestApplication;
6 | import io.jexxa.testapplication.applicationservice.IncrementApplicationService;
7 | import kong.unirest.Unirest;
8 | import org.junit.jupiter.api.AfterEach;
9 | import org.junit.jupiter.api.BeforeEach;
10 | import org.junit.jupiter.api.Tag;
11 | import org.junit.jupiter.api.Test;
12 |
13 | import java.util.ArrayList;
14 | import java.util.List;
15 | import java.util.stream.IntStream;
16 | import java.util.stream.Stream;
17 |
18 | import static io.jexxa.common.facade.utils.function.ThrowingConsumer.exceptionCollector;
19 | import static org.junit.jupiter.api.Assertions.assertEquals;
20 | import static org.junit.jupiter.api.Assertions.assertTrue;
21 |
22 | @Tag(TestConstants.INTEGRATION_TEST)
23 | class MultipleRESTClientsIT
24 | {
25 | private static final String METHOD_GET_SIMPLE_VALUE = "increment";
26 | private static final int MAX_COUNTER = 1000;
27 | private static final int MAX_THREADS = 5;
28 |
29 | private IncrementApplicationService applicationService;
30 | private JexxaMain jexxaMain;
31 |
32 |
33 | @BeforeEach
34 | void setUp()
35 | {
36 | jexxaMain = new JexxaMain(JexxaTestApplication.class);
37 | jexxaMain.disableBanner()
38 | .bind(RESTfulRPCAdapter.class).to(IncrementApplicationService.class)
39 | .start();
40 |
41 | applicationService = jexxaMain.getInstanceOfPort(IncrementApplicationService.class);
42 | }
43 |
44 | @Test
45 | void synchronizeMultipleClients()
46 | {
47 | //Arrange
48 | applicationService.setMaxCounter(MAX_COUNTER);
49 | List expectedResult = IntStream.rangeClosed(1, MAX_COUNTER)
50 | .boxed()
51 | .toList();
52 |
53 | var clientPool = Stream.generate(() -> new Thread(this::incrementService))
54 | .limit(MAX_THREADS)
55 | .toList();
56 |
57 | var exceptionList = new ArrayList();
58 |
59 | //Act
60 | clientPool.forEach(Thread::start);
61 |
62 | clientPool.forEach(exceptionCollector(Thread::join, exceptionList));
63 |
64 |
65 | //Assert
66 | assertEquals(expectedResult, applicationService.getUsedCounter());
67 | assertTrue(exceptionList.isEmpty());
68 | }
69 |
70 | void incrementService()
71 | {
72 | while ( applicationService.getCounter() < MAX_COUNTER )
73 | {
74 | //Act
75 | var restPath = "http://localhost:7500/IncrementApplicationService/";
76 | var response = Unirest.post(restPath + METHOD_GET_SIMPLE_VALUE)
77 | .header(RESTConstants.CONTENT_TYPE, RESTConstants.APPLICATION_TYPE)
78 | .asJson();
79 | if (!response.isSuccess())
80 | {
81 | throw new IllegalArgumentException("HTTP Response Error: " + response.getStatus() + " " + response.getStatusText() );
82 | }
83 | }
84 | }
85 |
86 | @AfterEach
87 | void tearDown()
88 | {
89 | jexxaMain.stop();
90 | Unirest.shutDown();
91 | }
92 |
93 | }
94 |
--------------------------------------------------------------------------------
/jexxa-test/src/test/java/io/jexxa/jexxatest/integrationtest/messaging/JMSBindingIT.java:
--------------------------------------------------------------------------------
1 | package io.jexxa.jexxatest.integrationtest.messaging;
2 |
3 | import io.jexxa.common.drivingadapter.messaging.jms.JMSConfiguration;
4 | import io.jexxa.testapplication.domain.model.JexxaValueObject;
5 | import io.jexxa.jexxatest.JexxaIntegrationTest;
6 | import io.jexxa.jexxatest.application.JexxaITTestApplication;
7 | import org.junit.jupiter.api.AfterAll;
8 | import org.junit.jupiter.api.BeforeAll;
9 | import org.junit.jupiter.api.Test;
10 |
11 | import java.util.concurrent.TimeUnit;
12 |
13 | import static org.junit.jupiter.api.Assertions.assertEquals;
14 | import static org.junit.jupiter.api.Assertions.assertTrue;
15 |
16 | class JMSBindingIT {
17 |
18 | private static final JexxaIntegrationTest JEXXA_INTEGRATION_TEST = new JexxaIntegrationTest(JexxaITTestApplication.class);
19 | private static JMSBinding objectUnderTest;
20 |
21 | @BeforeAll
22 | static void initBeforeAll()
23 | {
24 | objectUnderTest = JEXXA_INTEGRATION_TEST.getBinding(JMSBinding.class);
25 | }
26 |
27 | @Test
28 | void testMessageListener()
29 | {
30 | //Arrange
31 | var testTopic = "TestTopic";
32 | var messageSender = objectUnderTest.getSender();
33 | var messageListener = objectUnderTest.getListener(testTopic, JMSConfiguration.MessagingType.TOPIC);
34 |
35 | //Act
36 | messageSender.send(new JexxaValueObject(42)).toTopic(testTopic).asJson();
37 |
38 | var result = messageListener
39 | .awaitMessage(5, TimeUnit.SECONDS)
40 | .pop(JexxaValueObject.class);
41 |
42 | //Assert
43 | assertEquals(new JexxaValueObject(42), result);
44 | assertTrue(messageListener.getAll().isEmpty());
45 | }
46 |
47 | @Test
48 | void testRegisterListener()
49 | {
50 | //Arrange
51 | var testTopic = "TestTopic";
52 | var messageSender = objectUnderTest.getSender();
53 | var messageListener = new JMSListener(testTopic, JMSConfiguration.MessagingType.TOPIC);
54 | objectUnderTest.registerListener(messageListener);
55 |
56 | //Act
57 | messageSender.send(new JexxaValueObject(42)).toTopic(testTopic).asJson();
58 |
59 | var result = messageListener
60 | .awaitMessage(5, TimeUnit.SECONDS)
61 | .pop(JexxaValueObject.class);
62 |
63 | //Assert
64 | assertEquals(new JexxaValueObject(42), result);
65 | assertTrue(messageListener.getAll().isEmpty());
66 | }
67 |
68 | @Test
69 | void testClearMessageListener()
70 | {
71 | //Arrange
72 | var testTopic = "TestTopic";
73 | var messageSender = objectUnderTest.getSender();
74 | var messageListener = objectUnderTest.getListener(testTopic, JMSConfiguration.MessagingType.TOPIC);
75 |
76 | //Act
77 | messageSender.send(new JexxaValueObject(42)).toTopic(testTopic).asJson();
78 | messageListener
79 | .awaitMessage(5, TimeUnit.SECONDS)
80 | .clear();
81 |
82 | messageSender.send(new JexxaValueObject(42)).toTopic(testTopic).asJson();
83 | var result = messageListener
84 | .awaitMessage(5, TimeUnit.SECONDS)
85 | .pop(JexxaValueObject.class);
86 |
87 | //Assert
88 | assertEquals(new JexxaValueObject(42), result);
89 | assertTrue(messageListener.getAll().isEmpty());
90 | }
91 |
92 | @AfterAll
93 | static void tearDown()
94 | {
95 | JEXXA_INTEGRATION_TEST.shutDown();
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/jexxa-core/src/test/java/io/jexxa/core/FluentMonitorTest.java:
--------------------------------------------------------------------------------
1 | package io.jexxa.core;
2 |
3 | import io.jexxa.adapterapi.invocation.InvocationContext;
4 | import io.jexxa.adapterapi.invocation.monitor.AroundMonitor;
5 | import io.jexxa.testapplication.JexxaTestApplication;
6 | import io.jexxa.testapplication.applicationservice.SimpleApplicationService;
7 | import io.jexxa.testapplication.infrastructure.drivingadapter.generic.ProxyAdapter;
8 | import io.jexxa.testapplication.infrastructure.drivingadapter.generic.ProxyDrivingAdapter;
9 | import io.jexxa.testapplication.infrastructure.drivingadapter.portadapter.ProxyPortAdapter;
10 | import org.junit.jupiter.api.Test;
11 |
12 | import java.time.Duration;
13 | import java.util.concurrent.TimeUnit;
14 |
15 | import static io.jexxa.common.healthcheck.HealthIndicators.timeoutIndicator;
16 | import static org.awaitility.Awaitility.await;
17 | import static org.junit.jupiter.api.Assertions.assertFalse;
18 | import static org.junit.jupiter.api.Assertions.assertThrows;
19 | import static org.junit.jupiter.api.Assertions.assertTrue;
20 |
21 | class FluentMonitorTest {
22 |
23 | @Test
24 | void beforeMonitor()
25 | {
26 | //Arrange
27 | var jexxaMain = new JexxaMain(JexxaTestApplication.class);
28 | var maxTimeout = Duration.ofSeconds(2);
29 | var boundedContext = jexxaMain.getBoundedContext();
30 |
31 | jexxaMain.bind(ProxyDrivingAdapter.class).to(ProxyPortAdapter.class);
32 |
33 | //Act
34 | jexxaMain.monitor(ProxyPortAdapter.class).with(timeoutIndicator(maxTimeout));
35 |
36 | var firstResult = boundedContext.isHealthy();
37 |
38 | await()
39 | .atMost(3, TimeUnit.SECONDS)
40 | .pollInterval(100, TimeUnit.MILLISECONDS)
41 | .until(() -> !boundedContext.isHealthy());
42 |
43 | // / Assert
44 | assertTrue(firstResult); // After start status should be healthy
45 | assertFalse(boundedContext.isHealthy());
46 | }
47 |
48 | @Test
49 | void aroundMonitor()
50 | {
51 | //Arrange
52 | var jexxaMain = new JexxaMain(JexxaTestApplication.class);
53 | var boundedContext = jexxaMain.getBoundedContext();
54 | var objectUnderTest = jexxaMain.getInstanceOfPort(SimpleApplicationService.class);
55 |
56 | var proxyAdapter = new ProxyAdapter();
57 | proxyAdapter.register(objectUnderTest);
58 | jexxaMain.monitor(SimpleApplicationService.class).with(new ExceptionIndicator());
59 |
60 | // Act
61 | assertThrows(NullPointerException.class, () -> proxyAdapter.invoke(objectUnderTest::throwNullPointerException));
62 |
63 | // Assert
64 | assertFalse(boundedContext.isHealthy());
65 | }
66 |
67 | private static class ExceptionIndicator extends AroundMonitor {
68 |
69 | private RuntimeException occurredException;
70 |
71 | @Override
72 | public boolean healthy() {
73 | return occurredException == null;
74 | }
75 |
76 | @Override
77 | public String getStatusMessage() {
78 | if (occurredException == null)
79 | {
80 | return "All fine!";
81 | }
82 | return occurredException.getMessage();
83 | }
84 |
85 | @Override
86 | public void around(InvocationContext invocationContext) {
87 | try {
88 | invocationContext.invoke();
89 | } catch (RuntimeException e)
90 | {
91 | occurredException = e;
92 | throw e;
93 | }
94 | }
95 | }
96 | }
--------------------------------------------------------------------------------
/jexxa-test/src/main/java/io/jexxa/jexxatest/architecture/PortsAndAdapters.java:
--------------------------------------------------------------------------------
1 | package io.jexxa.jexxatest.architecture;
2 |
3 |
4 | import com.tngtech.archunit.core.importer.ImportOption;
5 | import com.tngtech.archunit.library.Architectures;
6 | import io.jexxa.addend.applicationcore.Aggregate;
7 | import io.jexxa.addend.applicationcore.Repository;
8 |
9 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
10 | import static com.tngtech.archunit.library.Architectures.onionArchitecture;
11 |
12 | /**
13 | * These tests validate the access direction of an onion architecture which is as follows:
14 | *
15 | * @startuml
16 | *
17 | * package "DrivingAdapter " #DDDDDD {
18 | * [DrivingAdapter] #Implementation
19 | * }
20 | * package ApplicationCore #DDDDDD {
21 | * [ApplicationService] #lightgreen
22 | * [DomainService] #lightgreen
23 | * [Domain] #lightgreen
24 | * }
25 | * package "DrivenAdapter " #DDDDDD {
26 | * [DrivenAdapter] #Implementation
27 | * }
28 | * [DrivingAdapter] -r--> [ApplicationService] : uses
29 | * [DrivingAdapter] -r--> [DomainService] : uses
30 | * [ApplicationService] -down-> [DomainService] : uses
31 | * [ApplicationService] -down-> [Domain] : uses
32 | * [DomainService] -r-> [Domain] : uses
33 | * [DrivenAdapter] .u..> [Domain]
34 | * [DrivenAdapter] .u..> [DomainService] : implements
35 | *
36 | * @enduml
37 | * ....
38 | */
39 | public class PortsAndAdapters extends ProjectContent {
40 |
41 | private final Architectures.OnionArchitecture onionArchitecture;
42 | @SuppressWarnings("unused")
43 | PortsAndAdapters(Class> project)
44 | {
45 | this(project, ImportOption.Predefined.DO_NOT_INCLUDE_TESTS);
46 | }
47 |
48 | protected PortsAndAdapters(Class> project, ImportOption importOption)
49 | {
50 | super(project, importOption);
51 | this.onionArchitecture = onionArchitecture()
52 | .domainModels(project().getPackage().getName() + ".domain..")
53 | .domainServices(project().getPackage().getName() +".domainservice..")
54 | .applicationServices(project().getPackage().getName() + ".applicationservice..");
55 |
56 | this.onionArchitecture.allowEmptyShould(true);
57 | }
58 |
59 | public PortsAndAdapters addDrivenAdapterPackage(String drivenAdapterPackage)
60 | {
61 | onionArchitecture.adapter("drivenAdapter." + drivenAdapterPackage, project().getPackageName() + "." + "infrastructure.drivenadapter." + drivenAdapterPackage + "..");
62 | return this;
63 | }
64 |
65 | public PortsAndAdapters addDrivingAdapterPackage(String drivingAdapterPackage)
66 | {
67 | onionArchitecture.adapter("drivingAdapter." + drivingAdapterPackage, project().getPackageName() + ".infrastructure.drivingadapter." + drivingAdapterPackage + "..");
68 | return this;
69 | }
70 |
71 | @Override
72 | public void validate()
73 | {
74 | validatePortsAndAdapters();
75 | validateDrivingAdapterAccess();
76 | }
77 |
78 | private void validatePortsAndAdapters()
79 | {
80 | onionArchitecture.check(importedClasses());
81 | }
82 |
83 | private void validateDrivingAdapterAccess()
84 | {
85 | //Don't access a repository or aggregate directly from a driving adapter
86 | var drivingAdapter = noClasses().that()
87 | .resideInAnyPackage(project().getPackageName() + ".infrastructure.drivingadapter..").should()
88 | .dependOnClassesThat().areAnnotatedWith(Aggregate.class).orShould()
89 | .dependOnClassesThat().areAnnotatedWith(Repository.class)
90 | .allowEmptyShould(true);
91 | drivingAdapter.check(importedClasses());
92 | }
93 | }
--------------------------------------------------------------------------------
/jexxa-core/src/test/java/io/jexxa/core/factory/DependencyScannerTest.java:
--------------------------------------------------------------------------------
1 | package io.jexxa.core.factory;
2 |
3 | import io.jexxa.TestConstants;
4 | import io.jexxa.adapterapi.drivingadapter.IDrivingAdapter;
5 | import io.jexxa.testapplication.annotation.UnavailableDuringRuntime;
6 | import io.jexxa.testapplication.annotation.ValidApplicationService;
7 | import io.jexxa.testapplication.applicationservice.SimpleApplicationService;
8 | import org.junit.jupiter.api.Tag;
9 | import org.junit.jupiter.api.Test;
10 | import org.junit.jupiter.api.parallel.Execution;
11 | import org.junit.jupiter.api.parallel.ExecutionMode;
12 |
13 | import java.util.List;
14 |
15 | import static io.jexxa.core.factory.PackageConstants.JEXXA_APPLICATION_SERVICE;
16 | import static io.jexxa.core.factory.PackageConstants.JEXXA_DRIVING_ADAPTER;
17 | import static org.junit.jupiter.api.Assertions.assertEquals;
18 | import static org.junit.jupiter.api.Assertions.assertFalse;
19 | import static org.junit.jupiter.api.Assertions.assertThrows;
20 | import static org.junit.jupiter.api.Assertions.assertTrue;
21 |
22 | @Execution(ExecutionMode.CONCURRENT)
23 | @Tag(TestConstants.UNIT_TEST)
24 | class DependencyScannerTest
25 | {
26 | @Test
27 | void findAnnotatedClassesWithinPackage() {
28 | //Arrange
29 | var objectUnderTest = new DependencyScanner();
30 |
31 | //Act
32 | var applicationServiceList = objectUnderTest.
33 | acceptPackage(JEXXA_APPLICATION_SERVICE).
34 | getClassesWithAnnotation(ValidApplicationService.class);
35 |
36 | //Assert
37 | assertFalse(applicationServiceList.isEmpty());
38 | assertTrue(applicationServiceList
39 | .stream()
40 | .anyMatch(SimpleApplicationService.class::isAssignableFrom));
41 |
42 | }
43 |
44 |
45 | @Test
46 | void findAnnotatedClassesFailsWithinPackage() {
47 | //Arrange
48 | var invalidPackageName = "io.invalid.package";
49 | var objectUnderTest = new DependencyScanner();
50 |
51 | //Act
52 | var applicationServiceList = objectUnderTest.
53 | acceptPackage(invalidPackageName).
54 | getClassesWithAnnotation(ValidApplicationService.class);
55 |
56 | //Assert
57 | assertTrue(applicationServiceList.isEmpty());
58 | }
59 |
60 | @Test
61 | void getClassesImplementingInterface() {
62 | //Arrange
63 | var objectUnderTest = new DependencyScanner();
64 | objectUnderTest.acceptPackage(JEXXA_DRIVING_ADAPTER);
65 |
66 |
67 | //Act
68 | List> drivingAdapters = objectUnderTest.getClassesImplementing(IDrivingAdapter.class);
69 |
70 | //Assert
71 | assertFalse(drivingAdapters.isEmpty());
72 | }
73 |
74 |
75 | @Test
76 | void getClassesImplementingInterfaceInSpecificPackage() {
77 | //Arrange
78 | var objectUnderTest = new DependencyScanner();
79 | var packageName = "io.jexxa.common.drivingadapter.messaging";
80 |
81 | //Act
82 | List> drivingAdapters = objectUnderTest.
83 | acceptPackage(packageName).
84 | getClassesImplementing(IDrivingAdapter.class);
85 |
86 | //Assert
87 | assertFalse(drivingAdapters.isEmpty());
88 | assertEquals(1, drivingAdapters.size());
89 | }
90 |
91 | @Test
92 | void handleAnnotationUnavailableDuringRuntime()
93 | {
94 | //Arrange
95 | var objectUnderTest = new DependencyScanner();
96 |
97 | //Act
98 | assertThrows(IllegalArgumentException.class, () -> objectUnderTest.getClassesWithAnnotation(UnavailableDuringRuntime.class));
99 | }
100 |
101 | }
102 |
--------------------------------------------------------------------------------
/jexxa-core/src/test/java/io/jexxa/core/BoundedContextTest.java:
--------------------------------------------------------------------------------
1 | package io.jexxa.core;
2 |
3 |
4 | import io.jexxa.TestConstants;
5 | import io.jexxa.adapterapi.drivingadapter.HealthCheck;
6 | import io.jexxa.testapplication.JexxaTestApplication;
7 | import org.junit.jupiter.api.BeforeEach;
8 | import org.junit.jupiter.api.Tag;
9 | import org.junit.jupiter.api.Test;
10 | import org.junit.jupiter.api.function.Executable;
11 | import org.junit.jupiter.api.parallel.Execution;
12 | import org.junit.jupiter.api.parallel.ExecutionMode;
13 |
14 | import java.time.Duration;
15 | import java.util.concurrent.TimeUnit;
16 |
17 | import static org.awaitility.Awaitility.await;
18 | import static org.junit.jupiter.api.Assertions.assertEquals;
19 | import static org.junit.jupiter.api.Assertions.assertFalse;
20 | import static org.junit.jupiter.api.Assertions.assertTimeout;
21 | import static org.junit.jupiter.api.Assertions.assertTrue;
22 |
23 | @Execution(ExecutionMode.SAME_THREAD)
24 | @Tag(TestConstants.UNIT_TEST)
25 | class BoundedContextTest
26 | {
27 | private JexxaMain jexxaMain;
28 | private BoundedContext objectUnderTest;
29 |
30 |
31 | @BeforeEach
32 | void init()
33 | {
34 | jexxaMain = new JexxaMain(JexxaTestApplication.class);
35 |
36 | objectUnderTest = jexxaMain
37 | .disableBanner()
38 | .getBoundedContext();
39 | }
40 |
41 | @Test
42 | void shutdown()
43 | {
44 | //Arrange
45 | var thread = new Thread(jexxaMain::run);
46 | thread.start();
47 |
48 | await().atMost(1, TimeUnit.SECONDS)
49 | .until(() -> (objectUnderTest != null && objectUnderTest.isRunning()));
50 |
51 | //Act
52 | objectUnderTest.stop();
53 |
54 | //Assert
55 | assertTimeout(Duration.ofSeconds(1), (Executable) thread::join);
56 | }
57 |
58 | @Test
59 | void testIsHealthy()
60 | {
61 | //Arrange
62 | objectUnderTest.registerHealthCheck(new SimpleHealthCheck(true));
63 |
64 | //Assert
65 | assertTrue(objectUnderTest.isHealthy());
66 | assertEquals(1, objectUnderTest.diagnostics().size());
67 | assertTrue(objectUnderTest.diagnostics().getFirst().isHealthy());
68 | }
69 |
70 | @Test
71 | void testIsUnhealthy()
72 | {
73 | //Arrange
74 | objectUnderTest.registerHealthCheck(new SimpleHealthCheck(true));
75 | objectUnderTest.registerHealthCheck(new SimpleHealthCheck(false));
76 |
77 | //Assert
78 | assertFalse(objectUnderTest.isHealthy());
79 | assertEquals(2, objectUnderTest.diagnostics().size());
80 | assertTrue(objectUnderTest.diagnostics().get(0).isHealthy());
81 | assertFalse(objectUnderTest.diagnostics().get(1).isHealthy());
82 | }
83 |
84 | @Test
85 | void testJexxaVersion()
86 | {
87 | //Arrange --
88 |
89 | //Act
90 | var jexxaVersion = objectUnderTest.jexxaVersion();
91 |
92 | //Assert
93 | assertFalse(jexxaVersion.version().isEmpty());
94 | assertFalse(jexxaVersion.buildTimestamp().isEmpty());
95 | assertFalse(jexxaVersion.projectName().isEmpty());
96 | assertFalse(jexxaVersion.repository().isEmpty());
97 | }
98 |
99 | public static class SimpleHealthCheck extends HealthCheck
100 | {
101 | private final boolean isHealthy;
102 |
103 | public SimpleHealthCheck( boolean isHealthy )
104 | {
105 | this.isHealthy = isHealthy;
106 | }
107 |
108 | @Override
109 | public boolean healthy()
110 | {
111 | return isHealthy;
112 | }
113 |
114 | @Override
115 | public String getStatusMessage() {
116 | return "";
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/jexxa-core/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 |
6 | jexxa
7 | io.jexxa
8 | 9.0.2-SNAPSHOT
9 |
10 |
11 | jexxa-core
12 |
13 | Jexxa-Core
14 |
15 |
16 | The Apache Software License, Version 2.0
17 | https://www.apache.org/licenses/LICENSE-2.0.txt
18 | repo
19 |
20 |
21 |
22 |
23 | 4.2.4-SNAPSHOT
24 | repplix_Jexxa
25 |
26 |
27 |
28 |
29 |
30 |
31 | io.github.classgraph
32 | classgraph
33 | ${classgraph.version}
34 | compile
35 |
36 |
37 |
38 | io.jexxa.common
39 | common-adapters
40 | ${jexxa.common.adapters.version}
41 |
42 |
43 |
44 |
45 | org.junit.platform
46 | junit-platform-launcher
47 | ${junit.platform.launcher.version}
48 | test
49 |
50 |
51 | org.junit.jupiter
52 | junit-jupiter-engine
53 | ${junit.jupiter.engine.version}
54 | test
55 |
56 |
57 | org.junit.jupiter
58 | junit-jupiter-params
59 | ${junit.jupiter.params.version}
60 | test
61 |
62 |
63 | org.slf4j
64 | slf4j-simple
65 | ${slf4j.simple.version}
66 | test
67 |
68 |
69 | org.awaitility
70 | awaitility-groovy
71 | ${awaitility.version}
72 | test
73 |
74 |
75 |
76 | org.postgresql
77 | postgresql
78 | ${postgres.version}
79 | test
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | org.codehaus.mojo
88 | templating-maven-plugin
89 |
90 |
91 | filter-src
92 |
93 | filter-sources
94 |
95 |
96 |
97 |
98 |
99 |
100 | org.apache.maven.plugins
101 | maven-jar-plugin
102 |
103 |
104 | Jar Tests Package
105 | package
106 |
107 | test-jar
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/jexxa-web/src/test/resources/jexxa-application.properties:
--------------------------------------------------------------------------------
1 | #suppress inspection "UnusedProperty" for whole file
2 |
3 | ##########################################
4 | # Adjust system properties #
5 | ##########################################
6 | #io.jexxa.user.timezone=UTC
7 |
8 | ##########################################
9 | #Settings for JMSAdapter and JMSSender #
10 | ##########################################
11 | java.naming.factory.initial=org.apache.activemq.jndi.ActiveMQInitialContextFactory
12 | java.naming.provider.url=tcp://localhost:61616
13 | #java.naming.provider.url=vm://localhost?broker.persistent=false #for local jms provider
14 | #java.naming.client.id="MyClient" # Client ID for JMS connection
15 | java.naming.user=artemis
16 | java.naming.password=simetraehcapa
17 | #java.naming.file.user=/pathTo/jdbcUsername
18 | #java.naming.file.password=/pathTo/jdbcPassword
19 |
20 | ##########################################
21 | #Settings for RESTfulRPCAdapter #
22 | ##########################################
23 | io.jexxa.rest.host=localhost
24 | io.jexxa.rest.port=7500
25 |
26 | # Static files related settings
27 | # Following path relates to Classpath
28 | # io.jexxa.rest.static.files.root=/public
29 |
30 | # You can also configure an external path
31 | # io.jexxa.rest.static.files.root=src/main/resources/public
32 | # io.jexxa.rest.static.files.external=true
33 |
34 | # HTTPS related settings
35 | # See here how to create a keystore including a certificate:
36 | # https://docs.oracle.com/cd/E35976_01/server.740/es_admin/src/tadm_ssl_jetty_keystore.html
37 | #io.jexxa.rest.https.port=8080
38 | #io.jexxa.rest.keystore.location=keystore.jks
39 | #io.jexxa.rest.keystore.password=test123
40 | #io.jexxa.rest.keystore.file.password=/pathTo/fileWithSecret
41 |
42 | # OpenAPI Support.
43 | # Enable OpenAPI support by defining a path.
44 | #io.jexxa.rest.openapi.path=swagger-docs
45 |
46 | ##########################################
47 | #Settings for JDBCConnection #
48 | ##########################################
49 | io.jexxa.jdbc.driver=org.postgresql.Driver
50 | io.jexxa.jdbc.url=jdbc:postgresql://localhost:5432/hellojexxa
51 | io.jexxa.jdbc.username=admin
52 | io.jexxa.jdbc.password=admin
53 | #io.jexxa.jdbc.file.username=/pathTo/usernameFile
54 | #io.jexxa.jdbc.file.password=/pathTo/passwordFile
55 |
56 | # Following setting is only required if you want to auto-create your database, and it is supported via connection URL. In this case you have to define a valid default URL (e.g. for testing purpose)
57 | io.jexxa.jdbc.autocreate.database=jdbc:postgresql://localhost:5432/postgres
58 |
59 | # Following setting is only required if you want to auto-create your tables (e.g. for testing purpose)
60 | io.jexxa.jdbc.autocreate.table=true
61 |
62 |
63 | ##########################################################
64 | # Application specific information: #
65 | ##########################################################
66 | # Import other properties file
67 | #io.jexxa.config.import=path/to/other/config.properties
68 |
69 | ##########################################################
70 | # You can get this information from maven properties. #
71 | # Please note that maven's resource filtering must be #
72 | # enabled. #
73 | # #
74 | # #
75 | # #
76 | # src/main/resources #
77 | # true #
78 | # #
79 | # #
80 | ##########################################################
81 | io.jexxa.context.name=${project.name}
82 | io.jexxa.context.version=${project.name}
83 | io.jexxa.context.repository=${project.scm.developerConnection}
84 | io.jexxa.context.build.timestamp=${build.timestamp}
85 |
86 |
--------------------------------------------------------------------------------
/jexxa-web/src/test/java/io/jexxa/drivingadapter/rest/HttpsRESTfulRPCAdapterIT.java:
--------------------------------------------------------------------------------
1 | package io.jexxa.drivingadapter.rest;
2 |
3 | import io.jexxa.testapplication.applicationservice.SimpleApplicationService;
4 | import kong.unirest.Unirest;
5 | import kong.unirest.apache.ApacheClient;
6 | import org.apache.http.conn.ssl.NoopHostnameVerifier;
7 | import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
8 | import org.apache.http.impl.client.CloseableHttpClient;
9 | import org.apache.http.impl.client.HttpClients;
10 | import org.apache.http.ssl.SSLContextBuilder;
11 | import org.junit.jupiter.api.AfterEach;
12 | import org.junit.jupiter.api.BeforeEach;
13 | import org.junit.jupiter.params.ParameterizedTest;
14 | import org.junit.jupiter.params.provider.MethodSource;
15 |
16 | import javax.net.ssl.SSLContext;
17 | import java.security.KeyManagementException;
18 | import java.security.KeyStoreException;
19 | import java.security.NoSuchAlgorithmException;
20 | import java.util.Properties;
21 | import java.util.stream.Stream;
22 |
23 | import static org.junit.jupiter.api.Assertions.assertEquals;
24 | import static org.junit.jupiter.api.Assertions.assertNotNull;
25 |
26 | class HttpsRESTfulRPCAdapterIT
27 | {
28 | private static final String METHOD_GET_SIMPLE_VALUE = "getSimpleValue";
29 |
30 | private static final int DEFAULT_VALUE = 42;
31 | private final SimpleApplicationService simpleApplicationService = new SimpleApplicationService();
32 |
33 | @SuppressWarnings("unused")
34 | // Run the tests with Port 0 (random port and port 8090)
35 | static Stream httpsPorts() {
36 | return Stream.of(0,8090);
37 | }
38 |
39 | @BeforeEach
40 | void initTest() throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException
41 | {
42 | SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(new TrustSelfSignedStrategy()).build();
43 |
44 | CloseableHttpClient customHttpClient = HttpClients.custom().setSSLContext(sslContext)
45 | .setSSLHostnameVerifier(new NoopHostnameVerifier()).build();
46 |
47 | Unirest.config().httpClient(ApacheClient.builder(customHttpClient));
48 |
49 | Unirest.config().sslContext(sslContext);
50 | Unirest.config().hostnameVerifier(new NoopHostnameVerifier());
51 | }
52 |
53 | @ParameterizedTest
54 | @MethodSource("httpsPorts")
55 | void testHTTPSConnectionRandomPort(Integer httpsPort)
56 | {
57 | //Arrange
58 | var properties = new Properties();
59 | var defaultHost = "0.0.0.0";
60 |
61 | properties.put(JexxaWebProperties.JEXXA_REST_HOST, defaultHost);
62 | properties.put(JexxaWebProperties.JEXXA_REST_PORT, Integer.toString(0));
63 | properties.put(JexxaWebProperties.JEXXA_REST_HTTPS_PORT, httpsPort.toString());
64 | properties.put(JexxaWebProperties.JEXXA_REST_KEYSTORE_PASSWORD, "test123");
65 | properties.put(JexxaWebProperties.JEXXA_REST_KEYSTORE, "certificate/keystore.jks");
66 | properties.put(JexxaWebProperties.JEXXA_REST_OPEN_API_PATH, "swagger-docs");
67 |
68 | var objectUnderTest = RESTfulRPCAdapter.createAdapter(properties);
69 | objectUnderTest.register(simpleApplicationService);
70 | objectUnderTest.start();
71 |
72 | String restPath = "https://localhost:" + objectUnderTest.getHTTPSPort() + "/SimpleApplicationService/";
73 |
74 | objectUnderTest.bannerInformation(properties);
75 |
76 | //Act
77 | Integer result = Unirest.get(restPath + METHOD_GET_SIMPLE_VALUE)
78 | .header(RESTConstants.CONTENT_TYPE, RESTConstants.APPLICATION_TYPE)
79 | .asObject(Integer.class).getBody();
80 |
81 |
82 | //Assert
83 | assertNotNull(result);
84 | assertEquals(DEFAULT_VALUE, simpleApplicationService.getSimpleValue());
85 | assertEquals(simpleApplicationService.getSimpleValue(), result.intValue() );
86 | }
87 |
88 | @AfterEach
89 | void tearDown()
90 | {
91 | Unirest.shutDown();
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/jexxa-web/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 |
6 | jexxa
7 | io.jexxa
8 | 9.0.2-SNAPSHOT
9 |
10 |
11 | jexxa-web
12 |
13 |
14 | The Apache Software License, Version 2.0
15 | https://www.apache.org/licenses/LICENSE-2.0.txt
16 | repo
17 |
18 |
19 |
20 | Jexxa-Web
21 |
22 |
23 |
24 |
25 | io.jexxa
26 | jexxa-core
27 | ${project.version}
28 | compile
29 |
30 |
31 | com.google.code.gson
32 | gson
33 | ${gson.version}
34 |
35 |
36 |
37 |
38 | io.javalin
39 | javalin
40 | ${javalin.version}
41 | compile
42 |
43 |
44 |
45 | io.smallrye.config
46 | smallrye-config-core
47 | ${smallrye.config.core.version}
48 |
49 |
50 |
51 |
52 | io.smallrye
53 | smallrye-open-api-core
54 | ${smallrye.openapi.core.version}
55 |
56 |
57 |
58 |
59 |
60 |
61 | org.junit.platform
62 | junit-platform-launcher
63 | ${junit.platform.launcher.version}
64 | test
65 |
66 |
67 | org.junit.jupiter
68 | junit-jupiter-engine
69 | ${junit.jupiter.engine.version}
70 | test
71 |
72 |
73 | org.junit.jupiter
74 | junit-jupiter-params
75 | ${junit.jupiter.params.version}
76 | test
77 |
78 |
79 | org.slf4j
80 | slf4j-simple
81 | ${slf4j.simple.version}
82 | test
83 |
84 |
85 | org.awaitility
86 | awaitility-groovy
87 | ${awaitility.version}
88 | test
89 |
90 |
91 | io.jexxa
92 | jexxa-core
93 | ${project.version}
94 | test-jar
95 | test
96 |
97 |
98 | com.konghq
99 | unirest-java
100 | ${unirest.java.version}
101 | test
102 |
103 |
104 |
105 | commons-codec
106 | commons-codec
107 |
108 |
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/jexxa-test/src/test/java/io/jexxa/jexxatest/JexxaTestTest.java:
--------------------------------------------------------------------------------
1 | package io.jexxa.jexxatest;
2 |
3 | import io.jexxa.core.factory.InvalidAdapterException;
4 | import io.jexxa.jexxatest.application.DomainEventPublisher;
5 | import io.jexxa.testapplication.JexxaTestApplication;
6 | import io.jexxa.testapplication.applicationservice.ApplicationServiceWithInvalidDrivenAdapters;
7 | import io.jexxa.testapplication.domain.model.JexxaAggregateRepository;
8 | import io.jexxa.testapplication.domain.model.JexxaDomainEvent;
9 | import io.jexxa.testapplication.domain.model.JexxaValueObject;
10 | import io.jexxa.testapplication.domainservice.BootstrapJexxaAggregates;
11 | import io.jexxa.testapplication.domainservice.InvalidConstructorParameterService;
12 | import io.jexxa.testapplication.domainservice.NotImplementedService;
13 | import io.jexxa.testapplication.infrastructure.drivenadapter.persistence.JexxaAggregateRepositoryImpl;
14 | import org.junit.jupiter.api.BeforeEach;
15 | import org.junit.jupiter.api.Test;
16 |
17 | import static io.jexxa.jexxatest.JexxaTest.getJexxaTest;
18 | import static org.junit.jupiter.api.Assertions.assertEquals;
19 | import static org.junit.jupiter.api.Assertions.assertFalse;
20 | import static org.junit.jupiter.api.Assertions.assertThrows;
21 | import static org.junit.jupiter.api.Assertions.assertTrue;
22 |
23 |
24 | class JexxaTestTest
25 | {
26 | private JexxaTest jexxaTest;
27 |
28 | @BeforeEach
29 | void setUp()
30 | {
31 | //Arrange
32 | jexxaTest = getJexxaTest(JexxaTestApplication.class);
33 | }
34 |
35 | @Test
36 | void requestInvalidPort()
37 | {
38 | assertThrows(InvalidAdapterException.class, () -> jexxaTest.getInstanceOfPort(ApplicationServiceWithInvalidDrivenAdapters.class));
39 | }
40 |
41 | @Test
42 | void validateRepository()
43 | {
44 | //Arrange
45 | var jexxaRepository = jexxaTest.getRepository(JexxaAggregateRepository.class);
46 |
47 | //Act
48 | jexxaTest.getInstanceOfPort(BootstrapJexxaAggregates.class).initDomainData();
49 |
50 | //Assert
51 | assertFalse(jexxaRepository.get().isEmpty());
52 | }
53 |
54 | @Test
55 | void validateRepositoryIsInterface()
56 | {
57 | //Act/Assert
58 | assertThrows(IllegalArgumentException.class, () -> jexxaTest.getRepository(JexxaAggregateRepositoryImpl.class));
59 | }
60 |
61 |
62 | @Test
63 | void recordDomainEvent()
64 | {
65 | //Arrange
66 | var domainEvent = new JexxaDomainEvent(new JexxaValueObject(1));
67 | var objectUnderTest = jexxaTest.getDomainEventRecorder(JexxaDomainEvent.class, DomainEventPublisher::subscribe);
68 |
69 | //Act
70 | DomainEventPublisher.publish(domainEvent);
71 |
72 |
73 | //Assert MessageRecorder
74 | assertFalse(objectUnderTest.get().isEmpty());
75 | assertEquals(1, objectUnderTest.get().size());
76 | assertEquals(domainEvent, objectUnderTest.get().getFirst());
77 | }
78 |
79 |
80 | @Test
81 | void recordAllDomainEvent()
82 | {
83 | //Arrange
84 | var domainEvent = new JexxaDomainEvent(new JexxaValueObject(1));
85 | var objectUnderTest = jexxaTest.getDomainEventRecorder(DomainEventPublisher::subscribe);
86 |
87 | //Act
88 | DomainEventPublisher.publish(domainEvent);
89 |
90 |
91 | //Assert MessageRecorder
92 | assertFalse(objectUnderTest.get().isEmpty());
93 | assertEquals(1, objectUnderTest.get().size());
94 | assertEquals(domainEvent, objectUnderTest.get().getFirst());
95 | }
96 |
97 |
98 |
99 | @Test
100 | void repositoryNotAvailable()
101 | {
102 | //Act/Assert
103 | assertThrows( IllegalArgumentException.class, () -> jexxaTest.getRepository(NotImplementedService.class) );
104 |
105 | //Act/Assert
106 | assertThrows( InvalidAdapterException.class, () -> jexxaTest.getRepository(InvalidConstructorParameterService.class) );
107 | }
108 |
109 | @Test
110 | void jexxaTestProperties()
111 | {
112 | //Act/Assert
113 | assertTrue( jexxaTest.getProperties().containsKey("io.jexxa.test.project") );
114 | }
115 |
116 |
117 | }
118 |
--------------------------------------------------------------------------------
/jexxa-core/src/main/java/io/jexxa/core/BoundedContext.java:
--------------------------------------------------------------------------------
1 | package io.jexxa.core;
2 |
3 | import io.jexxa.adapterapi.drivingadapter.Diagnostics;
4 | import io.jexxa.adapterapi.drivingadapter.HealthCheck;
5 | import io.jexxa.properties.JexxaCoreProperties;
6 |
7 | import java.time.Clock;
8 | import java.time.Duration;
9 | import java.time.Instant;
10 | import java.util.ArrayList;
11 | import java.util.List;
12 | import java.util.Objects;
13 |
14 | import static io.jexxa.common.facade.logger.SLF4jLogger.getLogger;
15 |
16 |
17 | public final class BoundedContext
18 | {
19 | private boolean isRunning = false;
20 | private boolean isWaiting = false;
21 | private final String contextName;
22 | private final Clock clock = Clock.systemUTC();
23 | private final Instant startTime;
24 | private final JexxaMain jexxaMain;
25 | private final List healthChecks = new ArrayList<>();
26 |
27 | BoundedContext(final String contextName, JexxaMain jexxaMain)
28 | {
29 | this.startTime = clock.instant();
30 | this.contextName = Objects.requireNonNull(contextName);
31 | this.jexxaMain = Objects.requireNonNull(jexxaMain);
32 | }
33 |
34 | public Duration uptime()
35 | {
36 | return Duration.between(startTime, clock.instant());
37 | }
38 |
39 | public String contextName()
40 | {
41 | return contextName;
42 | }
43 |
44 | public VersionInfo jexxaVersion()
45 | {
46 | return JexxaVersion.getJexxaVersion();
47 | }
48 |
49 | public VersionInfo contextVersion()
50 | {
51 | var properties = jexxaMain.getProperties();
52 |
53 | return VersionInfo.of()
54 | .version(properties.getProperty(JexxaCoreProperties.JEXXA_CONTEXT_VERSION, ""))
55 | .repository(properties.getProperty(JexxaCoreProperties.JEXXA_CONTEXT_REPOSITORY, ""))
56 | .buildTimestamp(properties.getProperty(JexxaCoreProperties.JEXXA_CONTEXT_BUILD_TIMESTAMP, ""))
57 | .projectName(properties.getProperty(JexxaCoreProperties.JEXXA_CONTEXT_NAME, ""))
58 | .create();
59 | }
60 |
61 |
62 | public synchronized boolean isRunning()
63 | {
64 | return isRunning;
65 | }
66 |
67 | /**
68 | * Returns true if all HealthChecks returns true.
69 | * If at least one HealthCheck returns false, this method returns false as well
70 | *
71 | * @return True if all HealthChecks return true, otherwise false.
72 | */
73 | public boolean isHealthy()
74 | {
75 | var isHealthy = healthChecks.stream()
76 | .map(HealthCheck::healthy)
77 | .filter( element -> !element)
78 | .findFirst();
79 |
80 | return isHealthy.orElse(true);
81 | }
82 |
83 | public List diagnostics()
84 | {
85 | return healthChecks
86 | .stream()
87 | .map(HealthCheck::getDiagnostics)
88 | .toList();
89 | }
90 |
91 | void registerHealthCheck( HealthCheck healthCheck)
92 | {
93 | healthChecks.add(healthCheck);
94 | }
95 |
96 | synchronized JexxaMain waitForShutdown()
97 | {
98 | if (!isRunning)
99 | {
100 | return jexxaMain;
101 | }
102 |
103 | setupSignalHandler();
104 | isWaiting = true;
105 |
106 | try
107 | {
108 | while ( isWaiting ) {
109 | this.wait();
110 | }
111 | }
112 | catch (InterruptedException e)
113 | {
114 | Thread.currentThread().interrupt();
115 | throw new IllegalStateException(e);
116 | }
117 |
118 | return jexxaMain;
119 | }
120 |
121 | synchronized void start()
122 | {
123 | isRunning = true;
124 | }
125 |
126 | synchronized void stop()
127 | {
128 | isRunning = false;
129 | internalShutdown();
130 | }
131 |
132 | private void setupSignalHandler() {
133 | Runtime.getRuntime().addShutdownHook(new Thread(() -> {
134 | getLogger(JexxaMain.class).info("Shutdown signal received ...");
135 | jexxaMain.stop();
136 | }));
137 | }
138 |
139 | private synchronized void internalShutdown()
140 | {
141 | if ( isWaiting )
142 | {
143 | isWaiting = false;
144 | notifyAll();
145 | }
146 | }
147 |
148 | }
149 |
--------------------------------------------------------------------------------
/jexxa-core/src/main/java/io/jexxa/core/convention/AdapterConvention.java:
--------------------------------------------------------------------------------
1 | package io.jexxa.core.convention;
2 |
3 | import java.lang.reflect.Modifier;
4 | import java.util.Arrays;
5 | import java.util.List;
6 | import java.util.Properties;
7 |
8 | public final class AdapterConvention
9 | {
10 | public static void validate(Class clazz)
11 | {
12 | if (
13 | isDefaultConstructorAvailable(clazz)
14 | || isPropertiesConstructorAvailable(clazz)
15 | || isDefaultFactoryMethodAvailable(clazz)
16 | || isPropertiesFactoryMethodAvailable(clazz)
17 | )
18 | {
19 | return;
20 | }
21 |
22 | throw new AdapterConventionViolation(clazz);
23 | }
24 |
25 |
26 | public static boolean isPortAdapter(Class
port, List acceptedInfrastructure)
27 | {
28 |
29 | //Check constructor with one single application/domain service
30 | if ( Arrays.stream(port.getConstructors())
31 | .filter(constructor -> constructor.getParameterTypes().length == 1)
32 | .anyMatch(constructor -> !constructor.getParameterTypes()[0].isInterface())
33 | &&
34 | isInInfrastructurePackage(port, acceptedInfrastructure))
35 | {
36 | return true;
37 | }
38 |
39 | //Check constructor with one single application/domain service and a properties-parameter
40 | return Arrays.stream(port.getConstructors())
41 | .filter(constructor -> constructor.getParameterTypes().length == 2)
42 | .anyMatch(constructor -> !constructor.getParameterTypes()[0].isInterface()
43 | && constructor.getParameterTypes()[1].isAssignableFrom(Properties.class)
44 | )
45 | &&
46 | isInInfrastructurePackage(port, acceptedInfrastructure);
47 | }
48 |
49 | private static boolean isDefaultConstructorAvailable(Class clazz)
50 | {
51 | try
52 | {
53 | clazz.getConstructor();
54 | return true; //Default constructor available
55 | }
56 | catch (NoSuchMethodException | SecurityException _)
57 | {
58 | //If exception is thrown go on to check if another suitable constructor is available
59 | }
60 |
61 | return false;
62 | }
63 |
64 | private static boolean isPropertiesConstructorAvailable(Class clazz)
65 | {
66 | try
67 | {
68 | clazz.getConstructor(Properties.class);
69 | return true; //Constructor with Properties argument available
70 | }
71 | catch (NoSuchMethodException | SecurityException _)
72 | {
73 | //If exception is thrown go on to check if another suitable constructor is available
74 | }
75 | return false;
76 | }
77 |
78 | private static boolean isDefaultFactoryMethodAvailable(Class clazz)
79 | {
80 | var factoryMethods = Arrays
81 | .stream(clazz.getMethods())
82 | .filter(method -> Modifier.isStatic(method.getModifiers()))
83 | .filter(method -> method.getReturnType().isAssignableFrom(clazz))
84 | .toList();
85 |
86 | return factoryMethods.stream().anyMatch(method -> method.getParameterCount() == 0); //Factory method with no arguments available
87 | }
88 |
89 | private static boolean isPropertiesFactoryMethodAvailable(Class clazz)
90 | {
91 | var factoryMethods = Arrays
92 | .stream(clazz.getMethods())
93 | .filter(method -> Modifier.isStatic(method.getModifiers()))
94 | .filter(method -> method.getReturnType().isAssignableFrom(clazz))
95 | .toList();
96 |
97 | return factoryMethods.stream().anyMatch(method -> (
98 | method.getParameterCount() == 1 &&
99 | method.getParameterTypes()[0].isAssignableFrom(Properties.class))); //Factory method with Properties argument available
100 | }
101 |
102 | private static boolean isInInfrastructurePackage( Class clazz, List acceptedInfrastructure)
103 | {
104 | return acceptedInfrastructure
105 | .stream()
106 | .anyMatch( element -> clazz.getPackage().toString().contains( element ) );
107 | }
108 |
109 |
110 | private AdapterConvention()
111 | {
112 | //Private Constructor
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/jexxa-test/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | jexxa
5 | io.jexxa
6 | 9.0.2-SNAPSHOT
7 |
8 | 4.0.0
9 |
10 | io.jexxa.jexxatest
11 | jexxa-test
12 | Jexxa-Test
13 |
14 |
15 | The Apache Software License, Version 2.0
16 | https://www.apache.org/licenses/LICENSE-2.0.txt
17 | repo
18 |
19 |
20 |
21 |
22 |
23 | io.jexxa
24 | jexxa-core
25 | ${project.version}
26 | compile
27 |
28 |
29 | com.tngtech.archunit
30 | archunit
31 | ${archunit.version}
32 | compile
33 |
34 |
35 | io.jexxa.addend
36 | Addend
37 | ${addend.version}
38 | compile
39 |
40 |
41 | com.konghq
42 | unirest-java
43 | ${unirest.java.version}
44 |
45 |
46 | commons-codec
47 | commons-codec
48 |
49 |
50 |
51 |
52 | org.awaitility
53 | awaitility-groovy
54 | ${awaitility.version}
55 |
56 |
57 |
58 |
59 |
60 | org.slf4j
61 | slf4j-simple
62 | ${slf4j.simple.version}
63 | test
64 |
65 |
66 | org.postgresql
67 | postgresql
68 | ${postgres.version}
69 | test
70 |
71 |
72 | org.junit.platform
73 | junit-platform-launcher
74 | ${junit.platform.launcher.version}
75 | test
76 |
77 |
78 | org.junit.jupiter
79 | junit-jupiter-engine
80 | ${junit.jupiter.engine.version}
81 | test
82 |
83 |
84 | org.junit.jupiter
85 | junit-jupiter-params
86 | ${junit.jupiter.params.version}
87 | test
88 |
89 |
90 | io.jexxa
91 | jexxa-web
92 | ${project.version}
93 | test
94 |
95 |
96 | org.apache.activemq
97 | artemis-jms-client
98 | ${activemq.artemis.client.version}
99 | test
100 |
101 |
102 |
103 |
104 |
105 | io.jexxa
106 | jexxa-core
107 | ${project.version}
108 | test-jar
109 | test
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/jexxa-web/src/test/java/io/jexxa/drivingadapter/rest/RESTfulRPCConventionTest.java:
--------------------------------------------------------------------------------
1 | package io.jexxa.drivingadapter.rest;
2 |
3 | import io.jexxa.TestConstants;
4 | import io.jexxa.testapplication.applicationservice.SimpleApplicationService;
5 | import org.junit.jupiter.api.BeforeEach;
6 | import org.junit.jupiter.api.Tag;
7 | import org.junit.jupiter.api.Test;
8 | import org.junit.jupiter.api.parallel.Execution;
9 | import org.junit.jupiter.api.parallel.ExecutionMode;
10 |
11 | import java.lang.reflect.Modifier;
12 | import java.util.ArrayList;
13 | import java.util.Arrays;
14 |
15 | import static org.junit.jupiter.api.Assertions.assertEquals;
16 | import static org.junit.jupiter.api.Assertions.assertFalse;
17 | import static org.junit.jupiter.api.Assertions.assertNotEquals;
18 | import static org.junit.jupiter.api.Assertions.assertNotNull;
19 | import static org.junit.jupiter.api.Assertions.assertThrows;
20 | import static org.junit.jupiter.api.Assertions.assertTrue;
21 |
22 | @Execution(ExecutionMode.CONCURRENT)
23 | @Tag(TestConstants.UNIT_TEST)
24 | class RESTfulRPCConventionTest
25 | {
26 | private RESTfulRPCConvention objectUnderTest;
27 | private SimpleApplicationService simpleApplicationService;
28 |
29 | @BeforeEach
30 | void setupTests()
31 | {
32 | simpleApplicationService = new SimpleApplicationService();
33 | simpleApplicationService.setSimpleValue(42);
34 | objectUnderTest = new RESTfulRPCConvention(simpleApplicationService);
35 | }
36 |
37 | @Test
38 | void validateGETCommands()
39 | {
40 | //Act
41 | var result = objectUnderTest.getGETCommands();
42 |
43 | //Assert
44 | //1. Check all conventions as defined in {@link RESTfulRPCModel}.
45 | assertFalse(result.isEmpty());
46 |
47 | //2. Check that all commands are marked as GET
48 | result.forEach(element -> assertEquals(RESTfulRPCConvention.RESTfulRPCMethod.HTTPCommand.GET,
49 | element.getHTTPCommand()));
50 |
51 | //3. Check URIs
52 | result.forEach(element -> assertEquals("/" + SimpleApplicationService.class.getSimpleName() + "/"+element.method().getName(),
53 | element.resourcePath()));
54 |
55 | //4. Check return types are NOT void
56 | result.forEach(element -> assertNotEquals(void.class, element.method().getReturnType()));
57 | }
58 |
59 | @Test
60 | void validatePOSTCommands()
61 | {
62 | //Act
63 | var result = objectUnderTest.getPOSTCommands();
64 |
65 | //Assert
66 | //1.Check all conventions as defined in {@link RESTfulRPCGenerator}.
67 | assertFalse(result.isEmpty());
68 |
69 | //2.Check that all commands are marked as GET
70 | result.forEach(element -> assertEquals(RESTfulRPCConvention.RESTfulRPCMethod.HTTPCommand.POST,
71 | element.getHTTPCommand()));
72 |
73 | //3.Check URIs
74 | result.forEach(element -> assertEquals("/" + SimpleApplicationService.class.getSimpleName() + "/"+element.method().getName(),
75 | element.resourcePath()));
76 |
77 | //4.Check return types are NOT void or Parameter > 0
78 | result.forEach(element -> assertTrue( (void.class.equals(element.method().getReturnType())
79 | || element.method().getParameterCount() > 0 )));
80 |
81 | }
82 |
83 | @Test
84 | void noStaticMethods()
85 | {
86 | //Arrange
87 | var staticMethods = Arrays.stream(simpleApplicationService.getClass().getMethods())
88 | .filter(method -> Modifier.isStatic(method.getModifiers()))
89 | .toList();
90 |
91 | //Act - get All methods
92 | var methods = new ArrayList();
93 | methods.addAll(objectUnderTest.getPOSTCommands());
94 | methods.addAll(objectUnderTest.getGETCommands());
95 |
96 | //Assert - that we get mbean methods without static methods
97 | assertNotNull(methods);
98 | assertTrue ( staticMethods.stream().allMatch(
99 | staticMethod -> methods.stream()
100 | .noneMatch( method -> method.method().getName().equals(staticMethod.getName()))
101 | )
102 | );
103 | }
104 |
105 | @Test
106 | void invalidApplicationService()
107 | {
108 | //Arrange
109 | var unsupportedApplicationService = new UnsupportedApplicationService();
110 |
111 | //Act / Assert
112 | assertThrows(IllegalArgumentException.class, () ->
113 | new RESTfulRPCConvention(unsupportedApplicationService)
114 | );
115 | }
116 |
117 | }
118 |
--------------------------------------------------------------------------------
/jexxa-web/src/main/java/io/jexxa/drivingadapter/rest/RESTfulRPCConvention.java:
--------------------------------------------------------------------------------
1 | package io.jexxa.drivingadapter.rest;
2 |
3 | import java.lang.reflect.Method;
4 | import java.lang.reflect.Modifier;
5 | import java.util.ArrayList;
6 | import java.util.Arrays;
7 | import java.util.HashSet;
8 | import java.util.List;
9 |
10 |
11 | /**
12 | * This class generates uniform IDs (URIs) for resources to be offered via REST
13 | * using a convention over configuration approach:
14 | *
15 | * Used conventions for URI:
16 | *
17 | * {@code URI: http://://}
18 | *
19 | * Example URI: http://localhost:7500/MyApplicationService/myMethod
20 | *
21 | * This implies the following conventions:
22 | *
23 | * - Simple name of a class must be unique within a single application
24 | * - Each class must have unique method names. Any method overloading is not supported
25 | * - Methods from base class `Object` are ignored
26 | *
27 | * GET - mapping:
28 | *
29 | *
30 | * - If a method returns a type != 'void' and has no arguments then it is mapped to a GET method
31 | *
32 | *
33 | * POST - mapping:
34 | *
35 | * - In all other cases
36 | *
37 | */
38 | class RESTfulRPCConvention
39 | {
40 | private final Object object;
41 |
42 | RESTfulRPCConvention(Object object)
43 | {
44 | this.object = object;
45 | validateUniqueURI();
46 | }
47 |
48 |
49 | record RESTfulRPCMethod(RESTfulRPCConvention.RESTfulRPCMethod.HTTPCommand httpCommand,
50 | String resourcePath,
51 | Method method) {
52 | enum HTTPCommand {GET, POST}
53 |
54 | HTTPCommand getHTTPCommand() {
55 | return httpCommand;
56 | }
57 | }
58 |
59 | List getGETCommands() {
60 |
61 | return getPublicMethods(object.getClass())
62 | .stream()
63 | .filter(element -> !Modifier.isStatic(element.getModifiers())) //Convention for all exposed methods
64 | .filter(element -> !(element.getReturnType().equals(void.class)) &&
65 | element.getParameterCount() == 0) // Convention for GET method
66 | .map(element ->
67 | new RESTfulRPCMethod(
68 | RESTfulRPCMethod.HTTPCommand.GET,
69 | generateURI(element),
70 | element))
71 | .toList();
72 | }
73 |
74 |
75 | List getPOSTCommands() {
76 |
77 | return getPublicMethods(object.getClass())
78 | .stream()
79 | .filter(element -> !Modifier.isStatic(element.getModifiers())) //Convention for all exposed methods
80 | .filter(element -> (element.getReturnType().equals(void.class) ||
81 | element.getParameterCount() > 0)) // Convention for POST method
82 | .map(element ->
83 | new RESTfulRPCMethod(
84 | RESTfulRPCMethod.HTTPCommand.POST,
85 | generateURI(element),
86 | element))
87 | .toList();
88 | }
89 |
90 |
91 | private String generateURI(Method method) {
92 | return "/" + method.getDeclaringClass().getSimpleName() + "/" + method.getName();
93 | }
94 |
95 | private List getPublicMethods(Class> clazz)
96 | {
97 | List result = new ArrayList<>(Arrays.asList(clazz.getMethods()));
98 | result.removeAll(Arrays.asList(Object.class.getMethods()));
99 |
100 | return result;
101 | }
102 |
103 | private void validateUniqueURI()
104 | {
105 | List publicMethods = getPublicMethods(object.getClass());
106 | List methodNames = new ArrayList<>();
107 |
108 | publicMethods.forEach(element -> methodNames.add(generateURI(element)));
109 |
110 | // Make a unique list (by converting it into a HashSet) and compare its size with size of publicMethods.
111 | // If it is not equal, URIs are not unique
112 | List uniqueNames = new ArrayList<>( new HashSet<>(methodNames) );
113 |
114 | if (uniqueNames.size() != methodNames.size() ) {
115 | throw new IllegalArgumentException("Method names are not unique of Object " + object.getClass().getSimpleName());
116 | }
117 | }
118 |
119 | public static RESTfulRPCConvention createRPCConvention(Object object)
120 | {
121 | return new RESTfulRPCConvention(object);
122 | }
123 | }
124 |
--------------------------------------------------------------------------------