GH-1 - Initial port of Moduliths project.
Basically the state of commit c7cf939 of https://github.com/moduliths/moduliths for further development under the Spring umbrella.
This commit is contained in:
49
.github/workflows/build.yaml
vendored
Normal file
49
.github/workflows/build.yaml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: Maven Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, 1.3.x ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build project
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
- name: Check out sources
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: 17
|
||||
cache: 'maven'
|
||||
|
||||
- name: Build with Maven
|
||||
run: ./mvnw -B
|
||||
|
||||
integrations:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
boot-version: ["2.5.14", "2.6.9", "3.0.0-SNAPSHOT"]
|
||||
name: Integration test (Boot ${{ matrix.boot-version }})
|
||||
needs: build
|
||||
steps:
|
||||
|
||||
- name: Check out sources
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: 17
|
||||
cache: 'maven'
|
||||
|
||||
- name: Build with Maven (Boot ${{ matrix.boot-version }})
|
||||
run: sed -i -e 's/2.7.1/${{ matrix.boot-version }}/g' ./pom.xml && ./mvnw dependency:list -B && ./mvnw -B
|
||||
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
target/
|
||||
.idea/
|
||||
.flattened-pom.xml
|
||||
.settings/
|
||||
*.iml
|
||||
.project
|
||||
.classpath
|
||||
.springBeans
|
||||
target/
|
||||
.factorypath
|
||||
|
||||
#IntelliJ Stuff
|
||||
.idea
|
||||
*.iml
|
||||
117
.mvn/wrapper/MavenWrapperDownloader.java
vendored
Normal file
117
.mvn/wrapper/MavenWrapperDownloader.java
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2007-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import java.net.*;
|
||||
import java.io.*;
|
||||
import java.nio.channels.*;
|
||||
import java.util.Properties;
|
||||
|
||||
public class MavenWrapperDownloader {
|
||||
|
||||
private static final String WRAPPER_VERSION = "0.5.5";
|
||||
/**
|
||||
* Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
|
||||
*/
|
||||
private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
|
||||
+ WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
|
||||
|
||||
/**
|
||||
* Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
|
||||
* use instead of the default one.
|
||||
*/
|
||||
private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
|
||||
".mvn/wrapper/maven-wrapper.properties";
|
||||
|
||||
/**
|
||||
* Path where the maven-wrapper.jar will be saved to.
|
||||
*/
|
||||
private static final String MAVEN_WRAPPER_JAR_PATH =
|
||||
".mvn/wrapper/maven-wrapper.jar";
|
||||
|
||||
/**
|
||||
* Name of the property which should be used to override the default download url for the wrapper.
|
||||
*/
|
||||
private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
|
||||
|
||||
public static void main(String args[]) {
|
||||
System.out.println("- Downloader started");
|
||||
File baseDirectory = new File(args[0]);
|
||||
System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
|
||||
|
||||
// If the maven-wrapper.properties exists, read it and check if it contains a custom
|
||||
// wrapperUrl parameter.
|
||||
File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
|
||||
String url = DEFAULT_DOWNLOAD_URL;
|
||||
if(mavenWrapperPropertyFile.exists()) {
|
||||
FileInputStream mavenWrapperPropertyFileInputStream = null;
|
||||
try {
|
||||
mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
|
||||
Properties mavenWrapperProperties = new Properties();
|
||||
mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
|
||||
url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
|
||||
} catch (IOException e) {
|
||||
System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
|
||||
} finally {
|
||||
try {
|
||||
if(mavenWrapperPropertyFileInputStream != null) {
|
||||
mavenWrapperPropertyFileInputStream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Ignore ...
|
||||
}
|
||||
}
|
||||
}
|
||||
System.out.println("- Downloading from: " + url);
|
||||
|
||||
File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
|
||||
if(!outputFile.getParentFile().exists()) {
|
||||
if(!outputFile.getParentFile().mkdirs()) {
|
||||
System.out.println(
|
||||
"- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
|
||||
}
|
||||
}
|
||||
System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
|
||||
try {
|
||||
downloadFileFromURL(url, outputFile);
|
||||
System.out.println("Done");
|
||||
System.exit(0);
|
||||
} catch (Throwable e) {
|
||||
System.out.println("- Error downloading");
|
||||
e.printStackTrace();
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void downloadFileFromURL(String urlString, File destination) throws Exception {
|
||||
if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
|
||||
String username = System.getenv("MVNW_USERNAME");
|
||||
char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
|
||||
Authenticator.setDefault(new Authenticator() {
|
||||
@Override
|
||||
protected PasswordAuthentication getPasswordAuthentication() {
|
||||
return new PasswordAuthentication(username, password);
|
||||
}
|
||||
});
|
||||
}
|
||||
URL website = new URL(urlString);
|
||||
ReadableByteChannel rbc;
|
||||
rbc = Channels.newChannel(website.openStream());
|
||||
FileOutputStream fos = new FileOutputStream(destination);
|
||||
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
|
||||
fos.close();
|
||||
rbc.close();
|
||||
}
|
||||
|
||||
}
|
||||
BIN
.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
BIN
.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
Binary file not shown.
2
.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
2
.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.1/apache-maven-3.6.1-bin.zip
|
||||
wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar
|
||||
201
LICENSE
Normal file
201
LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
2
application.yml
Normal file
2
application.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
changelog:
|
||||
repository: spring-projects-experimental/spring-modulith
|
||||
17
etc/ide/README.md
Normal file
17
etc/ide/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Spring Data Code Formatting Settings
|
||||
|
||||
This directory contains `eclipse-formatting.xml` and `intellij.importorder` settings files to be used with Eclipse and IntelliJ.
|
||||
|
||||
## Eclipse Setup
|
||||
|
||||
Import both files in Eclipse through the Preferences dialog.
|
||||
|
||||
## IntelliJ Setup
|
||||
|
||||
Use the IntelliJ [Eclipse Code Formatter](https://plugins.jetbrains.com/plugin/6546-eclipse-code-formatter) plugin to configure code formatting and import ordering with the newest Eclipse formatter version.
|
||||
|
||||
Additionally, make sure to configure your import settings in `Editor -> Code Style -> Java` with the following explicit settings:
|
||||
|
||||
* Use tab character indents
|
||||
* Class count to use import with `*`: 10
|
||||
* Names count to use static import with `*`: 1
|
||||
291
etc/ide/eclipse-formatting.xml
Normal file
291
etc/ide/eclipse-formatting.xml
Normal file
@@ -0,0 +1,291 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<profiles version="12">
|
||||
<profile kind="CodeFormatterProfile" name="Spring Data" version="12">
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.disabling_tag" value="@formatter:off"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_field" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.use_on_off_tags" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_ellipsis" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_multiple_fields" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression" value="80"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_binary_operator" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_array_initializer" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_package" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.continuation_indentation" value="2"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_binary_operator" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_package" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.compiler.source" value="1.7"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_line_comments" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.join_wrapped_lines" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_member_type" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.align_type_members_on_columns" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_unary_operator" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.indent_parameter_description" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.lineSplit" value="120"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indentation.size" value="2"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.enabling_tag" value="@formatter:on"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_assignment" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.compiler.problem.assertIdentifier" value="error"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.tabulation.char" value="tab"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_body" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_method" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_method_declaration" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_switch" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.compiler.problem.enumIdentifier" value="error"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_ellipsis" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_method_declaration" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.compact_else_if" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.indent_root_tags" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_constant" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.tabulation.size" value="2"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_empty_lines" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block_in_case" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.compiler.compliance" value="1.7"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer" value="2"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_unary_operator" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_binary_expression" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode" value="enabled"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_label" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_javadoc_comments" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.line_length" value="120"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_import_groups" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.wrap_before_binary_operator" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_block" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.join_lines_in_comments" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_compact_if" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_imports" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_html" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_source_code" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.compiler.codegen.targetPlatform" value="1.7"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_resources_in_try" value="80"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_header" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_block_comments" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_enum_constants" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_type_declaration" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_imports" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line" value="false"/>
|
||||
</profile>
|
||||
</profiles>
|
||||
8
etc/ide/intellij.importorder
Normal file
8
etc/ide/intellij.importorder
Normal file
@@ -0,0 +1,8 @@
|
||||
#Organize Import Order
|
||||
#Mon Nov 14 09:58:12 CET 2016
|
||||
5=com
|
||||
4=org
|
||||
3=javax
|
||||
2=java
|
||||
1=
|
||||
0=\#
|
||||
3
lombok.config
Normal file
3
lombok.config
Normal file
@@ -0,0 +1,3 @@
|
||||
lombok.nonNull.exceptionType = IllegalArgumentException
|
||||
lombok.log.fieldName = LOG
|
||||
lombok.addLombokGeneratedAnnotation = true
|
||||
28
moduliths-api/pom.xml
Normal file
28
moduliths-api/pom.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.moduliths</groupId>
|
||||
<artifactId>moduliths</artifactId>
|
||||
<version>1.4.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>Moduliths - API</name>
|
||||
<artifactId>moduliths-api</artifactId>
|
||||
|
||||
<properties>
|
||||
<module.name>org.moduliths.api</module.name>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
41
moduliths-api/src/main/java/org/moduliths/Module.java
Normal file
41
moduliths-api/src/main/java/org/moduliths/Module.java
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Annotation to customize information of a {@link Modulith} module.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@Target({ ElementType.PACKAGE, ElementType.ANNOTATION_TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Module {
|
||||
|
||||
String displayName() default "";
|
||||
|
||||
/**
|
||||
* List the names of modules that the module is allowed to depend on. Shared modules defined in {@link Modulith} will
|
||||
* be allowed, too.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String[] allowedDependencies() default {};
|
||||
}
|
||||
85
moduliths-api/src/main/java/org/moduliths/Modulith.java
Normal file
85
moduliths-api/src/main/java/org/moduliths/Modulith.java
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2018-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurationExcludeFilter;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.context.TypeExcludeFilter;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.ComponentScan.Filter;
|
||||
import org.springframework.context.annotation.FilterType;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
|
||||
/**
|
||||
* Defines a Spring Boot application to follow the Modulith structuring conventions.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@Documented
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Modulithic
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
@ComponentScan(excludeFilters = { //
|
||||
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
|
||||
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
|
||||
public @interface Modulith {
|
||||
|
||||
/**
|
||||
* A logical system name for documentation purposes.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@AliasFor(annotation = Modulithic.class)
|
||||
String systemName() default "";
|
||||
|
||||
/**
|
||||
* Whether to use fully qualified module names by default. If set to {@literal true}, hits will cause the module's
|
||||
* default names to be their complete package name instead of just the modulith-local one. This might be useful in
|
||||
* case {@link #additionalPackages()} pulls in packages that would cause module name conflicts, i.e. both root
|
||||
* packages declare a local sub-package of the same name.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@AliasFor(annotation = Modulithic.class)
|
||||
boolean useFullyQualifiedModuleNames() default false;
|
||||
|
||||
/**
|
||||
* The names of modules considered to be shared, i.e. which should always be included in the bootstrap no matter what.
|
||||
* Useful for code to contain commons Spring configuration and components.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@AliasFor(annotation = Modulithic.class)
|
||||
String[] sharedModules() default {};
|
||||
|
||||
/**
|
||||
* Defines which additional packages shall be considered as modulith base packages in addition to the one of the class
|
||||
* carrying this annotation.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@AliasFor(annotation = Modulithic.class)
|
||||
String[] additionalPackages() default {};
|
||||
}
|
||||
66
moduliths-api/src/main/java/org/moduliths/Modulithic.java
Normal file
66
moduliths-api/src/main/java/org/moduliths/Modulithic.java
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Defines a Spring Boot application to follow the Modulith structuring conventions.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@Documented
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Modulithic {
|
||||
|
||||
/**
|
||||
* A logical system name for documentation purposes.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String systemName() default "";
|
||||
|
||||
/**
|
||||
* Whether to use fully qualified module names by default. If set to {@literal true}, hits will cause the module's
|
||||
* default names to be their complete package name instead of just the modulith-local one. This might be useful in
|
||||
* case {@link #additionalPackages()} pulls in packages that would cause module name conflicts, i.e. both root
|
||||
* packages declare a local sub-package of the same name.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
boolean useFullyQualifiedModuleNames() default false;
|
||||
|
||||
/**
|
||||
* The names of modules considered to be shared, i.e. which should always be included in the bootstrap no matter what.
|
||||
* Useful for code to contain commons Spring configuration and components.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String[] sharedModules() default {};
|
||||
|
||||
/**
|
||||
* Defines which additional packages shall be considered as modulith base packages in addition to the one of the class
|
||||
* carrying this annotation.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String[] additionalPackages() default {};
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Annotation to mark a package as named interface of a {@link Module} (either implicit or explicitly annotated).
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@Documented
|
||||
@Target({ ElementType.PACKAGE, ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface NamedInterface {
|
||||
|
||||
/**
|
||||
* The name of the interface.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String[] value();
|
||||
}
|
||||
83
moduliths-core/pom.xml
Normal file
83
moduliths-core/pom.xml
Normal file
@@ -0,0 +1,83 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.moduliths</groupId>
|
||||
<artifactId>moduliths</artifactId>
|
||||
<version>1.4.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>Moduliths - Core</name>
|
||||
<artifactId>moduliths-core</artifactId>
|
||||
|
||||
<properties>
|
||||
<module.name>org.moduliths.core</module.name>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.moduliths</groupId>
|
||||
<artifactId>moduliths-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.tngtech.archunit</groupId>
|
||||
<artifactId>archunit</artifactId>
|
||||
<version>${archunit.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-commons</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- jMolecules -->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jmolecules</groupId>
|
||||
<artifactId>jmolecules-ddd</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jmolecules</groupId>
|
||||
<artifactId>jmolecules-events</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jmolecules.integrations</groupId>
|
||||
<artifactId>jmolecules-archunit</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.persistence</groupId>
|
||||
<artifactId>jakarta.persistence-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2019-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.moduliths.Modulithic;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link ModulithMetadata} backed by a {@link Modulithic} annotated type.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
class AnnotationModulithMetadata implements ModulithMetadata {
|
||||
|
||||
private final Class<?> modulithType;
|
||||
private final Modulithic annotation;
|
||||
|
||||
/**
|
||||
* Creates a {@link ModulithMetadata} inspecting {@link Modulithic} annotation or return {@link Optional#empty()} if
|
||||
* the type given does not carry the annotation.
|
||||
*
|
||||
* @param annotated must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public static Optional<ModulithMetadata> of(Class<?> annotated) {
|
||||
|
||||
Assert.notNull(annotated, "Modulith type must not be null!");
|
||||
|
||||
Modulithic annotation = AnnotatedElementUtils.findMergedAnnotation(annotated, Modulithic.class);
|
||||
|
||||
return Optional.ofNullable(annotation) //
|
||||
.map(it -> new AnnotationModulithMetadata(annotated, it));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ModulithMetadata#getModulithSource()
|
||||
*/
|
||||
@Override
|
||||
public Object getModulithSource() {
|
||||
return modulithType;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ModulithMetadata#getAdditionalPackages()
|
||||
*/
|
||||
@Override
|
||||
public List<String> getAdditionalPackages() {
|
||||
return Arrays.asList(annotation.additionalPackages());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ModulithMetadata#useFullyQualifiedModuleNames()
|
||||
*/
|
||||
@Override
|
||||
public boolean useFullyQualifiedModuleNames() {
|
||||
return annotation.useFullyQualifiedModuleNames();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ModulithMetadata#getSharedModuleNames()
|
||||
*/
|
||||
@Override
|
||||
public Stream<String> getSharedModuleNames() {
|
||||
return Arrays.stream(annotation.sharedModules());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ModulithMetadata#getSystemName()
|
||||
*/
|
||||
@Override
|
||||
public Optional<String> getSystemName() {
|
||||
|
||||
return Optional.of(annotation.systemName()) //
|
||||
.filter(StringUtils::hasText);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,582 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import static org.moduliths.model.Types.JavaXTypes.*;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Value;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.moduliths.model.Types.JMoleculesTypes;
|
||||
import org.moduliths.model.Types.SpringDataTypes;
|
||||
import org.moduliths.model.Types.SpringTypes;
|
||||
import org.springframework.data.repository.core.RepositoryMetadata;
|
||||
import org.springframework.data.repository.core.support.AbstractRepositoryMetadata;
|
||||
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
import com.tngtech.archunit.core.domain.JavaMethod;
|
||||
import com.tngtech.archunit.core.domain.JavaType;
|
||||
import com.tngtech.archunit.thirdparty.com.google.common.base.Supplier;
|
||||
import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers;
|
||||
|
||||
/**
|
||||
* A type that is architecturally relevant, i.e. it fulfills a significant role within the architecture.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public abstract class ArchitecturallyEvidentType {
|
||||
|
||||
private static Map<Key, ArchitecturallyEvidentType> CACHE = new HashMap<>();
|
||||
|
||||
private final @Getter JavaClass type;
|
||||
|
||||
/**
|
||||
* Creates a new {@link AbstractArchitecturallyEvidentType} for the given {@link JavaType} and {@link Classes} of
|
||||
* Spring components.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @param beanTypes must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public static ArchitecturallyEvidentType of(JavaClass type, Classes beanTypes) {
|
||||
|
||||
return CACHE.computeIfAbsent(Key.of(type, beanTypes), it -> {
|
||||
|
||||
List<ArchitecturallyEvidentType> delegates = new ArrayList<>();
|
||||
|
||||
if (JMoleculesTypes.isPresent()) {
|
||||
delegates.add(new JMoleculesArchitecturallyEvidentType(type));
|
||||
}
|
||||
|
||||
if (SpringDataTypes.isPresent()) {
|
||||
delegates.add(new SpringDataAwareArchitecturallyEvidentType(type, beanTypes));
|
||||
}
|
||||
|
||||
delegates.add(new SpringAwareArchitecturallyEvidentType(type));
|
||||
|
||||
return DelegatingType.of(type, delegates);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the abbreviated (i.e. every package fragment reduced to its first character) full name.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
String getAbbreviatedFullName() {
|
||||
return FormatableJavaClass.of(getType()).getAbbreviatedFullName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the type is an entity in the DDD sense.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
boolean isEntity() {
|
||||
return isJpaEntity().apply(getType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the type is considered an aggregate root in the DDD sense.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public abstract boolean isAggregateRoot();
|
||||
|
||||
/**
|
||||
* Returns whether the type is considered a repository in the DDD sense.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public abstract boolean isRepository();
|
||||
|
||||
public boolean isService() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isController() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isEventListener() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isConfigurationProperties() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns other types that are interesting in the context of the current {@link ArchitecturallyEvidentType}. For
|
||||
* example, for an event listener this might be the event types the particular listener is interested in.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Stream<JavaClass> getReferenceTypes() {
|
||||
return Stream.empty();
|
||||
}
|
||||
|
||||
public Stream<ReferenceMethod> getReferenceMethods() {
|
||||
return Stream.empty();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return type.getFullName();
|
||||
}
|
||||
|
||||
private static Stream<JavaClass> distinctByName(Stream<JavaClass> types) {
|
||||
|
||||
Set<String> names = new HashSet<>();
|
||||
|
||||
return types.flatMap(it -> {
|
||||
|
||||
if (names.contains(it.getFullName())) {
|
||||
return Stream.empty();
|
||||
} else {
|
||||
|
||||
names.add(it.getFullName());
|
||||
|
||||
return Stream.of(it);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static class SpringAwareArchitecturallyEvidentType extends ArchitecturallyEvidentType {
|
||||
|
||||
/**
|
||||
* Methods (meta-)annotated with @EventListener.
|
||||
*/
|
||||
private static final Predicate<JavaMethod> IS_ANNOTATED_EVENT_LISTENER = it -> //
|
||||
Types.isAnnotatedWith(SpringTypes.AT_EVENT_LISTENER).apply(it) //
|
||||
|| Types.isAnnotatedWith(SpringTypes.AT_TX_EVENT_LISTENER).apply(it);
|
||||
|
||||
/**
|
||||
* {@code ApplicationListener.onApplicationEvent(…)}
|
||||
*/
|
||||
private static final Predicate<JavaMethod> IS_IMPLEMENTING_EVENT_LISTENER = it -> //
|
||||
it.getOwner().isAssignableTo(SpringTypes.APPLICATION_LISTENER) //
|
||||
&& it.getName().equals("onApplicationEvent") //
|
||||
&& !it.reflect().isSynthetic();
|
||||
|
||||
private static final Predicate<JavaMethod> IS_EVENT_LISTENER = IS_ANNOTATED_EVENT_LISTENER
|
||||
.or(IS_IMPLEMENTING_EVENT_LISTENER);
|
||||
|
||||
public SpringAwareArchitecturallyEvidentType(JavaClass type) {
|
||||
super(type);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ArchitecturallyEvidentType#isAggregateRoot()
|
||||
*/
|
||||
@Override
|
||||
public boolean isAggregateRoot() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ArchitecturallyEvidentType#isRepository()
|
||||
*/
|
||||
@Override
|
||||
public boolean isRepository() {
|
||||
return Types.isAnnotatedWith(SpringTypes.AT_REPOSITORY).apply(getType());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ArchitecturallyEvidentType#isService()
|
||||
*/
|
||||
@Override
|
||||
public boolean isService() {
|
||||
return Types.isAnnotatedWith(SpringTypes.AT_SERVICE).apply(getType());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ArchitecturallyEvidentType#isController()
|
||||
*/
|
||||
@Override
|
||||
public boolean isController() {
|
||||
return Types.isAnnotatedWith(SpringTypes.AT_CONTROLLER).apply(getType());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ArchitecturallyEvidentType#isEventListener()
|
||||
*/
|
||||
@Override
|
||||
public boolean isEventListener() {
|
||||
return getType().getMethods().stream().anyMatch(IS_EVENT_LISTENER);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ArchitecturallyEvidentType#isConfigurationProperties()
|
||||
*/
|
||||
@Override
|
||||
public boolean isConfigurationProperties() {
|
||||
return Types.isAnnotatedWith(SpringTypes.AT_CONFIGURATION_PROPERTIES).apply(getType());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ArchitecturallyEvidentType#getOtherTypeReferences()
|
||||
*/
|
||||
@Override
|
||||
public Stream<JavaClass> getReferenceTypes() {
|
||||
|
||||
if (isEventListener()) {
|
||||
|
||||
return distinctByName(getType().getMethods().stream() //
|
||||
.filter(IS_EVENT_LISTENER) //
|
||||
.flatMap(it -> it.getRawParameterTypes().stream()))
|
||||
.sorted(Comparator.comparing(JavaClass::getSimpleName));
|
||||
}
|
||||
|
||||
return super.getReferenceTypes();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ArchitecturallyEvidentType#getReferenceMethods()
|
||||
*/
|
||||
@Override
|
||||
public Stream<ReferenceMethod> getReferenceMethods() {
|
||||
|
||||
if (!isEventListener()) {
|
||||
return super.getReferenceMethods();
|
||||
}
|
||||
|
||||
return getType().getMethods().stream() //
|
||||
.filter(IS_EVENT_LISTENER)
|
||||
.sorted(Comparator.comparing(JavaMethod::getName)
|
||||
.thenComparing(it -> it.getRawParameterTypes().size()))
|
||||
.map(ReferenceMethod::new);
|
||||
}
|
||||
}
|
||||
|
||||
static class SpringDataAwareArchitecturallyEvidentType extends ArchitecturallyEvidentType {
|
||||
|
||||
private final Classes beanTypes;
|
||||
|
||||
SpringDataAwareArchitecturallyEvidentType(JavaClass type, Classes beanTypes) {
|
||||
super(type);
|
||||
this.beanTypes = beanTypes;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ArchitecturallyEvidentType#isEntity()
|
||||
*/
|
||||
@Override
|
||||
public boolean isEntity() {
|
||||
|
||||
return super.isEntity() //
|
||||
|| getType().isAnnotatedWith("org.springframework.data.mongodb.core.mapping.Document");
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.DefaultArchitectuallyEvidentType#isAggregateRoot(org.moduliths.model.Classes)
|
||||
*/
|
||||
@Override
|
||||
public boolean isAggregateRoot() {
|
||||
return isEntity() && beanTypes.that(SpringDataTypes.isSpringDataRepository()).stream() //
|
||||
.map(JavaClass::reflect) //
|
||||
.map(AbstractRepositoryMetadata::getMetadata) //
|
||||
.map(RepositoryMetadata::getDomainType) //
|
||||
.anyMatch(it -> getType().isAssignableTo(it));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ArchitecturallyEvidentType#isRepository()
|
||||
*/
|
||||
@Override
|
||||
public boolean isRepository() {
|
||||
return SpringDataTypes.isSpringDataRepository().apply(getType());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ArchitecturallyEvidentType#isController()
|
||||
*/
|
||||
@Override
|
||||
public boolean isController() {
|
||||
return Types.isAnnotatedWith("org.springframework.data.rest.webmvc.BasePathAwareController").apply(getType());
|
||||
}
|
||||
}
|
||||
|
||||
static class JMoleculesArchitecturallyEvidentType extends ArchitecturallyEvidentType {
|
||||
|
||||
private static final Predicate<JavaMethod> IS_ANNOTATED_EVENT_LISTENER = Types
|
||||
.isAnnotatedWith(JMoleculesTypes.AT_DOMAIN_EVENT_HANDLER)::apply;
|
||||
|
||||
JMoleculesArchitecturallyEvidentType(JavaClass type) {
|
||||
super(type);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ArchitecturallyEvidentType#isEntity()
|
||||
*/
|
||||
@Override
|
||||
public boolean isEntity() {
|
||||
|
||||
JavaClass type = getType();
|
||||
|
||||
return Types.isAnnotatedWith(org.jmolecules.ddd.annotation.Entity.class).apply(type) || //
|
||||
type.isAssignableTo(org.jmolecules.ddd.types.Entity.class);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ArchitecturallyEvidentType#isAggregateRoot()
|
||||
*/
|
||||
@Override
|
||||
public boolean isAggregateRoot() {
|
||||
|
||||
JavaClass type = getType();
|
||||
|
||||
return Types.isAnnotatedWith(org.jmolecules.ddd.annotation.AggregateRoot.class).apply(type) //
|
||||
|| type.isAssignableTo(org.jmolecules.ddd.types.AggregateRoot.class);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ArchitecturallyEvidentType#isRepository()
|
||||
*/
|
||||
@Override
|
||||
public boolean isRepository() {
|
||||
|
||||
JavaClass type = getType();
|
||||
|
||||
return Types.isAnnotatedWith(org.jmolecules.ddd.annotation.Repository.class).apply(type)
|
||||
|| type.isAssignableTo(org.jmolecules.ddd.types.Repository.class);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ArchitecturallyEvidentType#isService()
|
||||
*/
|
||||
@Override
|
||||
public boolean isService() {
|
||||
|
||||
JavaClass type = getType();
|
||||
|
||||
return Types.isAnnotatedWith(org.jmolecules.ddd.annotation.Service.class).apply(type);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ArchitecturallyEvidentType#isEventListener()
|
||||
*/
|
||||
@Override
|
||||
public boolean isEventListener() {
|
||||
return getType().getMethods().stream().anyMatch(IS_ANNOTATED_EVENT_LISTENER);
|
||||
}
|
||||
}
|
||||
|
||||
static class DelegatingType extends ArchitecturallyEvidentType {
|
||||
|
||||
private final Supplier<Boolean> isAggregateRoot, isRepository, isEntity, isService, isController, isEventListener,
|
||||
isConfigurationProperties;
|
||||
private final Supplier<Collection<JavaClass>> referenceTypes;
|
||||
private final Supplier<Collection<ReferenceMethod>> referenceMethods;
|
||||
|
||||
DelegatingType(JavaClass type, Supplier<Boolean> isAggregateRoot,
|
||||
Supplier<Boolean> isRepository, Supplier<Boolean> isEntity, Supplier<Boolean> isService,
|
||||
Supplier<Boolean> isController, Supplier<Boolean> isEventListener, Supplier<Boolean> isConfigurationProperties,
|
||||
Supplier<Collection<JavaClass>> referenceTypes, Supplier<Collection<ReferenceMethod>> referenceMethods) {
|
||||
|
||||
super(type);
|
||||
|
||||
this.isAggregateRoot = isAggregateRoot;
|
||||
this.isRepository = isRepository;
|
||||
this.isEntity = isEntity;
|
||||
this.isService = isService;
|
||||
this.isController = isController;
|
||||
this.isEventListener = isEventListener;
|
||||
this.isConfigurationProperties = isConfigurationProperties;
|
||||
this.referenceTypes = referenceTypes;
|
||||
this.referenceMethods = referenceMethods;
|
||||
}
|
||||
|
||||
public static DelegatingType of(JavaClass type, List<ArchitecturallyEvidentType> types) {
|
||||
|
||||
Supplier<Boolean> isAggregateRoot = Suppliers
|
||||
.memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isAggregateRoot));
|
||||
|
||||
Supplier<Boolean> isRepository = Suppliers
|
||||
.memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isRepository));
|
||||
|
||||
Supplier<Boolean> isEntity = Suppliers
|
||||
.memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isEntity));
|
||||
|
||||
Supplier<Boolean> isService = Suppliers
|
||||
.memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isService));
|
||||
|
||||
Supplier<Boolean> isController = Suppliers
|
||||
.memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isController));
|
||||
|
||||
Supplier<Boolean> isEventListener = Suppliers
|
||||
.memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isEventListener));
|
||||
|
||||
Supplier<Boolean> isConfigurationProperties = Suppliers
|
||||
.memoize(() -> types.stream().anyMatch(ArchitecturallyEvidentType::isConfigurationProperties));
|
||||
|
||||
Supplier<Collection<JavaClass>> referenceTypes = Suppliers.memoize(() -> types.stream() //
|
||||
.flatMap(ArchitecturallyEvidentType::getReferenceTypes) //
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
Supplier<Collection<ReferenceMethod>> referenceMethods = Suppliers.memoize(() -> types.stream() //
|
||||
.flatMap(ArchitecturallyEvidentType::getReferenceMethods) //
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
return new DelegatingType(type, isAggregateRoot, isRepository, isEntity, isService, isController,
|
||||
isEventListener, isConfigurationProperties, referenceTypes, referenceMethods);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ArchitecturallyEvidentType#isAggregateRoot()
|
||||
*/
|
||||
// @Override
|
||||
@Override
|
||||
public boolean isAggregateRoot() {
|
||||
return isAggregateRoot.get();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ArchitecturallyEvidentType#isRepository()
|
||||
*/
|
||||
// @Override
|
||||
@Override
|
||||
public boolean isRepository() {
|
||||
return isRepository.get();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ArchitecturallyEvidentType#isEntity()
|
||||
*/
|
||||
@Override
|
||||
public boolean isEntity() {
|
||||
return isEntity.get();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ArchitecturallyEvidentType#isService()
|
||||
*/
|
||||
@Override
|
||||
public boolean isService() {
|
||||
return isService.get();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ArchitecturallyEvidentType#isController()
|
||||
*/
|
||||
@Override
|
||||
public boolean isController() {
|
||||
return isController.get();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ArchitecturallyEvidentType#isEventListener()
|
||||
*/
|
||||
@Override
|
||||
public boolean isEventListener() {
|
||||
return isEventListener.get();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ArchitecturallyEvidentType#isConfigurationProperties()
|
||||
*/
|
||||
@Override
|
||||
public boolean isConfigurationProperties() {
|
||||
return isConfigurationProperties.get();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ArchitecturallyEvidentType#getOtherTypeReferences()
|
||||
*/
|
||||
@Override
|
||||
public Stream<JavaClass> getReferenceTypes() {
|
||||
return distinctByName(referenceTypes.get().stream());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ArchitecturallyEvidentType#getReferenceMethods()
|
||||
*/
|
||||
@Override
|
||||
public Stream<ReferenceMethod> getReferenceMethods() {
|
||||
return referenceMethods.get().stream();
|
||||
}
|
||||
}
|
||||
|
||||
@Value(staticConstructor = "of")
|
||||
private static class Key {
|
||||
|
||||
JavaClass type;
|
||||
Classes beanTypes;
|
||||
}
|
||||
|
||||
@Value
|
||||
public final class ReferenceMethod {
|
||||
|
||||
private final JavaMethod method;
|
||||
|
||||
public boolean isAsync() {
|
||||
return method.isAnnotatedWith(SpringTypes.AT_ASYNC) || method.isMetaAnnotatedWith(SpringTypes.AT_ASYNC);
|
||||
}
|
||||
|
||||
public Optional<String> getTransactionPhase() {
|
||||
|
||||
return Optional.ofNullable(method.getAnnotationOfType(SpringTypes.AT_TX_EVENT_LISTENER))
|
||||
.map(it -> it.get("phase"))
|
||||
.map(Object::toString);
|
||||
}
|
||||
}
|
||||
}
|
||||
237
moduliths-core/src/main/java/org/moduliths/model/Classes.java
Normal file
237
moduliths-core/src/main/java/org/moduliths/model/Classes.java
Normal file
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
* Copyright 2018-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collector;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.tngtech.archunit.base.DescribedIterable;
|
||||
import com.tngtech.archunit.base.DescribedPredicate;
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
import com.tngtech.archunit.core.domain.JavaClasses;
|
||||
import com.tngtech.archunit.core.domain.JavaModifier;
|
||||
import com.tngtech.archunit.core.domain.JavaType;
|
||||
import com.tngtech.archunit.core.domain.properties.HasName;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
public class Classes implements DescribedIterable<JavaClass> {
|
||||
|
||||
private final List<JavaClass> classes;
|
||||
|
||||
/**
|
||||
* Creates a new {@link Classes} for the given {@link JavaClass}es.
|
||||
*
|
||||
* @param classes must not be {@literal null}.
|
||||
*/
|
||||
private Classes(List<JavaClass> classes) {
|
||||
|
||||
Assert.notNull(classes, "JavaClasses must not be null!");
|
||||
|
||||
this.classes = classes.stream() //
|
||||
.sorted(Comparator.comparing(JavaClass::getName)) //
|
||||
.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link Classes} for the given {@link JavaClass}es.
|
||||
*
|
||||
* @param classes must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
static Classes of(JavaClasses classes) {
|
||||
|
||||
return new Classes(StreamSupport.stream(classes.spliterator(), false) //
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link Classes} for the given {@link JavaClass}es.
|
||||
*
|
||||
* @param classes must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
static Classes of(List<JavaClass> classes) {
|
||||
return new Classes(classes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Collector} creating a {@link Classes} instance from a {@link Stream} of {@link JavaType}.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
static Collector<JavaClass, ?, Classes> toClasses() {
|
||||
return Collectors.collectingAndThen(Collectors.toList(), Classes::of);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@link Classes} that match the given {@link DescribedPredicate}.
|
||||
*
|
||||
* @param predicate must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
Classes that(DescribedPredicate<? super JavaClass> predicate) {
|
||||
|
||||
Assert.notNull(predicate, "Predicate must not be null!");
|
||||
|
||||
return classes.stream() //
|
||||
.filter((Predicate<JavaClass>) it -> predicate.apply(it)) //
|
||||
.collect(Collectors.collectingAndThen(Collectors.toList(), Classes::new));
|
||||
}
|
||||
|
||||
Classes and(Classes classes) {
|
||||
return and(classes.classes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Classes with the current elements and the given other ones combined.
|
||||
*
|
||||
* @param others must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
Classes and(Collection<JavaClass> others) {
|
||||
|
||||
Assert.notNull(others, "JavaClasses must not be null!");
|
||||
|
||||
if (others.isEmpty()) {
|
||||
return this;
|
||||
}
|
||||
|
||||
List<JavaClass> result = new ArrayList<>(classes);
|
||||
|
||||
others.forEach(it -> {
|
||||
if (!result.contains(it)) {
|
||||
result.add(it);
|
||||
}
|
||||
});
|
||||
|
||||
return new Classes(result);
|
||||
}
|
||||
|
||||
public Stream<JavaClass> stream() {
|
||||
return classes.stream();
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return !classes.iterator().hasNext();
|
||||
}
|
||||
|
||||
Optional<JavaClass> toOptional() {
|
||||
return isEmpty() ? Optional.empty() : Optional.of(classes.iterator().next());
|
||||
}
|
||||
|
||||
boolean contains(JavaClass type) {
|
||||
return !that(new SameClass(type)).isEmpty();
|
||||
}
|
||||
|
||||
boolean contains(String className) {
|
||||
return !that(HasName.Predicates.name(className)).isEmpty();
|
||||
}
|
||||
|
||||
JavaClass getRequiredClass(Class<?> type) {
|
||||
|
||||
return classes.stream() //
|
||||
.filter(it -> it.isEquivalentTo(type)) //
|
||||
.findFirst() //
|
||||
.orElseThrow(() -> new IllegalArgumentException(String.format("No JavaClass found for type %s!", type)));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see com.tngtech.archunit.base.HasDescription#getDescription()
|
||||
*/
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "";
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Iterable#iterator()
|
||||
*/
|
||||
@Override
|
||||
public Iterator<JavaClass> iterator() {
|
||||
return classes.iterator();
|
||||
}
|
||||
|
||||
String format() {
|
||||
return classes.stream() //
|
||||
.map(Classes::format) //
|
||||
.collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
String format(String basePackage) {
|
||||
return classes.stream() //
|
||||
.map(it -> Classes.format(it, basePackage)) //
|
||||
.collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
private static String format(JavaClass type) {
|
||||
return format(type, "");
|
||||
}
|
||||
|
||||
static String format(JavaClass type, String basePackage) {
|
||||
|
||||
Assert.notNull(type, "Type must not be null!");
|
||||
Assert.notNull(basePackage, "Base package must not be null!");
|
||||
|
||||
String prefix = type.getModifiers().contains(JavaModifier.PUBLIC) ? "+" : "o";
|
||||
String name = StringUtils.hasText(basePackage) //
|
||||
? type.getName().replace(basePackage, "…") //
|
||||
: type.getName();
|
||||
|
||||
return String.format(" %s %s", prefix, name);
|
||||
}
|
||||
|
||||
private static class SameClass extends DescribedPredicate<JavaClass> {
|
||||
|
||||
private final JavaClass reference;
|
||||
|
||||
public SameClass(JavaClass reference) {
|
||||
super(" is the same class as ");
|
||||
this.reference = reference;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see com.tngtech.archunit.base.DescribedPredicate#apply(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean apply(@Nullable JavaClass input) {
|
||||
return input != null && reference.getName().equals(input.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2019-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.moduliths.Modulith;
|
||||
import org.moduliths.Modulithic;
|
||||
import org.moduliths.model.Types.SpringTypes;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ModulithMetadata} representing the defaults of {@link Modulithic} but without the annotation
|
||||
* present.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
class DefaultModulithMetadata implements ModulithMetadata {
|
||||
|
||||
private static final Class<? extends Annotation> AT_SPRING_BOOT_APPLICATION = Types
|
||||
.loadIfPresent(SpringTypes.AT_SPRING_BOOT_APPLICATION);
|
||||
|
||||
private final @NonNull Object modulithSource;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ModulithMetadata} representing the defaults of a class annotated but not customized with
|
||||
* {@link Modulithic} or {@link Modulith}.
|
||||
*
|
||||
* @param annotated must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public static Optional<ModulithMetadata> of(Class<?> annotated) {
|
||||
|
||||
Assert.notNull(annotated, "Annotated type must not be null!");
|
||||
|
||||
return Optional.ofNullable(AT_SPRING_BOOT_APPLICATION) //
|
||||
.filter(it -> AnnotatedElementUtils.hasAnnotation(annotated, it)) //
|
||||
.map(__ -> new DefaultModulithMetadata(annotated));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ModulithMetadata} from the given package name.
|
||||
*
|
||||
* @param javaPackage must not be {@literal null} or empty.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.1
|
||||
*/
|
||||
public static ModulithMetadata of(String javaPackage) {
|
||||
|
||||
Assert.hasText(javaPackage, "Package name must not be null or empty!");
|
||||
|
||||
return new DefaultModulithMetadata(javaPackage);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ModulithMetadata#getModulithSource()
|
||||
*/
|
||||
@Override
|
||||
public Object getModulithSource() {
|
||||
return modulithSource;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ModulithMetadata#getAdditionalPackages()
|
||||
*/
|
||||
@Override
|
||||
public List<String> getAdditionalPackages() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ModulithMetadata#useFullyQualifiedModuleNames()
|
||||
*/
|
||||
@Override
|
||||
public boolean useFullyQualifiedModuleNames() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ModulithMetadata#getSharedModuleNames()
|
||||
*/
|
||||
@Override
|
||||
public Stream<String> getSharedModuleNames() {
|
||||
return Stream.empty();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ModulithMetadata#getSystemName()
|
||||
*/
|
||||
@Override
|
||||
public Optional<String> getSystemName() {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.tngtech.archunit.core.domain.JavaAccess;
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
import com.tngtech.archunit.core.domain.JavaModifier;
|
||||
|
||||
/**
|
||||
* A type that represents an event in a system.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 1.1
|
||||
*/
|
||||
@Value
|
||||
public class EventType {
|
||||
|
||||
private final JavaClass type;
|
||||
|
||||
/**
|
||||
* The sources that create that event. Includes static factory methods that return an instance of the event type
|
||||
* itself as well as constructor invocations, except ones from the factory methods.
|
||||
*/
|
||||
private final List<Source> sources;
|
||||
|
||||
/**
|
||||
* Creates a new {@link EventType} for the given {@link JavaClass}.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
*/
|
||||
public EventType(JavaClass type) {
|
||||
|
||||
Assert.notNull(type, "Type must not be null!");
|
||||
|
||||
this.type = type;
|
||||
|
||||
Stream<JavaAccess<?>> factoryMethodCalls = type.getMethods().stream()
|
||||
.filter(method -> method.getModifiers().contains(JavaModifier.STATIC))
|
||||
.filter(method -> method.getRawReturnType().equals(type))
|
||||
.flatMap(method -> method.getCallsOfSelf().stream());
|
||||
|
||||
Stream<JavaAccess<?>> constructorCalls = type.getConstructors().stream()
|
||||
.flatMap(constructor -> constructor.getCallsOfSelf().stream());
|
||||
|
||||
this.sources = Stream.concat(constructorCalls, factoryMethodCalls)
|
||||
.filter(call -> !call.getOriginOwner().equals(type))
|
||||
.map(JavaAccessSource::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public boolean hasSources() {
|
||||
return !this.sources.isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
import com.tngtech.archunit.thirdparty.com.google.common.base.Supplier;
|
||||
import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers;
|
||||
|
||||
/**
|
||||
* Wrapper around {@link JavaClass} that allows creating additional formatted names.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class FormatableJavaClass {
|
||||
|
||||
private static final Map<JavaClass, FormatableJavaClass> CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
private final JavaClass type;
|
||||
private final Supplier<String> abbreviatedName;
|
||||
|
||||
public static FormatableJavaClass of(JavaClass type) {
|
||||
return CACHE.computeIfAbsent(type, FormatableJavaClass::new);
|
||||
}
|
||||
|
||||
private FormatableJavaClass(JavaClass type) {
|
||||
|
||||
Assert.notNull(type, "JavaClass must not be null!");
|
||||
|
||||
this.type = type;
|
||||
this.abbreviatedName = Suppliers.memoize(() -> {
|
||||
|
||||
String abbreviatedPackage = Stream //
|
||||
.of(type.getPackageName().split("\\.")) //
|
||||
.map(it -> it.substring(0, 1)) //
|
||||
.collect(Collectors.joining("."));
|
||||
|
||||
return abbreviatedPackage.concat(".") //
|
||||
.concat(ClassUtils.getShortName(getFullName()));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the abbreviated (i.e. every package fragment reduced to its first character) full name, e.g.
|
||||
* {@code com.acme.MyType} will result in {@code c.a.MyType}.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public String getAbbreviatedFullName() {
|
||||
return abbreviatedName.get();
|
||||
}
|
||||
|
||||
public String getAbbreviatedFullName(@Nullable Module module) {
|
||||
|
||||
if (module == null) {
|
||||
return getAbbreviatedFullName();
|
||||
}
|
||||
|
||||
String basePackageName = module.getBasePackage().getName();
|
||||
|
||||
if (!StringUtils.hasText(basePackageName)) {
|
||||
return getAbbreviatedFullName();
|
||||
}
|
||||
|
||||
String typePackageName = type.getPackageName();
|
||||
|
||||
if (basePackageName.equals(typePackageName)) {
|
||||
return getAbbreviatedFullName();
|
||||
}
|
||||
|
||||
if (!typePackageName.startsWith(basePackageName)) {
|
||||
return getFullName();
|
||||
}
|
||||
|
||||
return abbreviate(basePackageName) //
|
||||
.concat(typePackageName.substring(basePackageName.length())) //
|
||||
.concat(".") //
|
||||
.concat(ClassUtils.getShortName(getFullName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type's full name.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public String getFullName() {
|
||||
return type.getName().replace("$", ".");
|
||||
}
|
||||
|
||||
private static String abbreviate(String source) {
|
||||
|
||||
return Stream //
|
||||
.of(source.split("\\.")) //
|
||||
.map(it -> it.substring(0, 1)) //
|
||||
.collect(Collectors.joining("."));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.tngtech.archunit.core.domain.JavaAccess;
|
||||
import com.tngtech.archunit.core.domain.JavaCodeUnit;
|
||||
|
||||
/**
|
||||
* A {@link Source} backed by an ArchUnit {@link JavaAccess}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 1.1
|
||||
*/
|
||||
class JavaAccessSource implements Source {
|
||||
|
||||
private final static Pattern LAMBDA_EXTRACTOR = Pattern.compile("lambda\\$(.*)\\$.*");
|
||||
|
||||
private final FormatableJavaClass type;
|
||||
private final JavaCodeUnit method;
|
||||
private final String name;
|
||||
|
||||
/**
|
||||
* Creates a new {@link JavaAccessSource} for the given {@link JavaAccess}.
|
||||
*
|
||||
* @param access must not be {@literal null}.
|
||||
*/
|
||||
public JavaAccessSource(JavaAccess<?> access) {
|
||||
|
||||
this.type = FormatableJavaClass.of(access.getOriginOwner());
|
||||
this.method = access.getOrigin();
|
||||
|
||||
String name = method.getName();
|
||||
Matcher matcher = LAMBDA_EXTRACTOR.matcher(name);
|
||||
|
||||
this.name = matcher.matches() ? matcher.group(1) : name;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.Source#toString(org.moduliths.model.Module)
|
||||
*/
|
||||
@Override
|
||||
public String toString(Module module) {
|
||||
|
||||
boolean noParameters = method.getRawParameterTypes().isEmpty();
|
||||
|
||||
return String.format("%s.%s(%s)", type.getAbbreviatedFullName(module), name, noParameters ? "" : "…");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import com.tngtech.archunit.base.DescribedIterable;
|
||||
import com.tngtech.archunit.base.DescribedPredicate;
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
import com.tngtech.archunit.core.domain.properties.CanBeAnnotated;
|
||||
import com.tngtech.archunit.thirdparty.com.google.common.base.Supplier;
|
||||
import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class JavaPackage implements DescribedIterable<JavaClass> {
|
||||
|
||||
private static final String PACKAGE_INFO_NAME = "package-info";
|
||||
|
||||
private final @Getter String name;
|
||||
private final Classes classes;
|
||||
private final Classes packageClasses;
|
||||
private final Supplier<Set<JavaPackage>> directSubPackages;
|
||||
|
||||
private JavaPackage(Classes classes, String name, boolean includeSubPackages) {
|
||||
|
||||
this.classes = classes;
|
||||
this.packageClasses = classes.that(resideInAPackage(includeSubPackages ? name.concat("..") : name));
|
||||
this.name = name;
|
||||
this.directSubPackages = Suppliers.memoize(() -> packageClasses.stream() //
|
||||
.map(it -> it.getPackageName()) //
|
||||
.filter(it -> !it.equals(name)) //
|
||||
.map(it -> extractDirectSubPackage(it)) //
|
||||
.distinct() //
|
||||
.map(it -> of(classes, it)) //
|
||||
.collect(Collectors.toSet()));
|
||||
}
|
||||
|
||||
public static JavaPackage of(Classes classes, String name) {
|
||||
return new JavaPackage(classes, name, true);
|
||||
}
|
||||
|
||||
public static boolean isPackageInfoType(JavaClass type) {
|
||||
return type.getSimpleName().equals(PACKAGE_INFO_NAME);
|
||||
}
|
||||
|
||||
public JavaPackage toSingle() {
|
||||
return new JavaPackage(classes, name, false);
|
||||
}
|
||||
|
||||
public String getLocalName() {
|
||||
return name.substring(name.lastIndexOf(".") + 1);
|
||||
}
|
||||
|
||||
public Collection<JavaPackage> getDirectSubPackages() {
|
||||
return directSubPackages.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all classes residing in the current package and potentially in sub-packages if the current package was
|
||||
* created to include them.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Classes getClasses() {
|
||||
return packageClasses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the direct sub-package name of the given candidate.
|
||||
*
|
||||
* @param candidate
|
||||
* @return
|
||||
*/
|
||||
private String extractDirectSubPackage(String candidate) {
|
||||
|
||||
if (candidate.length() <= name.length()) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
int subSubPackageIndex = candidate.indexOf('.', name.length() + 1);
|
||||
int endIndex = subSubPackageIndex == -1 ? candidate.length() : subSubPackageIndex;
|
||||
|
||||
return candidate.substring(0, endIndex);
|
||||
}
|
||||
|
||||
public Stream<JavaPackage> getSubPackagesAnnotatedWith(Class<? extends Annotation> annotation) {
|
||||
|
||||
return packageClasses.that(JavaClass.Predicates.simpleName(PACKAGE_INFO_NAME) //
|
||||
.and(CanBeAnnotated.Predicates.annotatedWith(annotation))).stream() //
|
||||
.map(JavaClass::getPackageName) //
|
||||
.distinct() //
|
||||
.map(it -> of(classes, it));
|
||||
}
|
||||
|
||||
public Classes that(DescribedPredicate<? super JavaClass> predicate) {
|
||||
return packageClasses.that(predicate);
|
||||
}
|
||||
|
||||
public boolean contains(JavaClass type) {
|
||||
return packageClasses.contains(type);
|
||||
}
|
||||
|
||||
public boolean contains(String className) {
|
||||
return packageClasses.contains(className);
|
||||
}
|
||||
|
||||
public Stream<JavaClass> stream() {
|
||||
return packageClasses.stream();
|
||||
}
|
||||
|
||||
public <A extends Annotation> Optional<A> getAnnotation(Class<A> annotationType) {
|
||||
|
||||
return packageClasses.that(JavaClass.Predicates.simpleName(PACKAGE_INFO_NAME) //
|
||||
.and(CanBeAnnotated.Predicates.annotatedWith(annotationType))) //
|
||||
.toOptional() //
|
||||
.map(it -> it.getAnnotationOfType(annotationType));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see com.tngtech.archunit.base.HasDescription#getDescription()
|
||||
*/
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return classes.getDescription();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Iterable#iterator()
|
||||
*/
|
||||
@Override
|
||||
public Iterator<JavaClass> iterator() {
|
||||
return classes.iterator();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
return new StringBuilder(name) //
|
||||
.append("\n") //
|
||||
.append(getClasses().format(name)) //
|
||||
.append('\n') //
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
833
moduliths-core/src/main/java/org/moduliths/model/Module.java
Normal file
833
moduliths-core/src/main/java/org/moduliths/model/Module.java
Normal file
@@ -0,0 +1,833 @@
|
||||
/*
|
||||
* Copyright 2018-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import static com.tngtech.archunit.base.DescribedPredicate.*;
|
||||
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
|
||||
import static java.lang.System.*;
|
||||
import static org.moduliths.model.Classes.*;
|
||||
import static org.moduliths.model.Types.*;
|
||||
import static org.moduliths.model.Types.JavaXTypes.*;
|
||||
import static org.moduliths.model.Types.SpringDataTypes.*;
|
||||
import static org.moduliths.model.Types.SpringTypes.*;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.moduliths.model.Types.JMoleculesTypes;
|
||||
import org.moduliths.model.Types.SpringTypes;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.tngtech.archunit.base.DescribedPredicate;
|
||||
import com.tngtech.archunit.core.domain.Dependency;
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
import com.tngtech.archunit.core.domain.JavaCodeUnit;
|
||||
import com.tngtech.archunit.core.domain.JavaConstructor;
|
||||
import com.tngtech.archunit.core.domain.JavaField;
|
||||
import com.tngtech.archunit.core.domain.JavaMember;
|
||||
import com.tngtech.archunit.core.domain.JavaMethod;
|
||||
import com.tngtech.archunit.core.domain.SourceCodeLocation;
|
||||
import com.tngtech.archunit.thirdparty.com.google.common.base.Supplier;
|
||||
import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@EqualsAndHashCode(doNotUseGetters = true)
|
||||
public class Module {
|
||||
|
||||
private final @Getter JavaPackage basePackage;
|
||||
private final ModuleInformation information;
|
||||
private final @Getter NamedInterfaces namedInterfaces;
|
||||
private final boolean useFullyQualifiedModuleNames;
|
||||
|
||||
private final Supplier<Classes> springBeans;
|
||||
private final Supplier<Classes> entities;
|
||||
private final Supplier<List<EventType>> publishedEvents;
|
||||
|
||||
Module(JavaPackage basePackage, boolean useFullyQualifiedModuleNames) {
|
||||
|
||||
this.basePackage = basePackage;
|
||||
this.information = ModuleInformation.of(basePackage);
|
||||
this.namedInterfaces = NamedInterfaces.discoverNamedInterfaces(basePackage);
|
||||
this.useFullyQualifiedModuleNames = useFullyQualifiedModuleNames;
|
||||
|
||||
this.springBeans = Suppliers.memoize(() -> filterSpringBeans(basePackage));
|
||||
this.entities = Suppliers.memoize(() -> findEntities(basePackage));
|
||||
this.publishedEvents = Suppliers.memoize(() -> findPublishedEvents());
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return useFullyQualifiedModuleNames ? basePackage.getName() : basePackage.getLocalName();
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return information.getDisplayName();
|
||||
}
|
||||
|
||||
public List<Module> getDependencies(Modules modules, DependencyType... type) {
|
||||
|
||||
return getAllModuleDependencies(modules) //
|
||||
.filter(it -> type.length == 0 ? true : Arrays.stream(type).anyMatch(it::hasType)) //
|
||||
.map(it -> modules.getModuleByType(it.target)) //
|
||||
.distinct() //
|
||||
.flatMap(it -> it.map(Stream::of).orElseGet(Stream::empty)) //
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all event types the current module exposes an event listener for.
|
||||
*
|
||||
* @param modules must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public List<JavaClass> getEventsListenedTo(Modules modules) {
|
||||
|
||||
Assert.notNull(modules, "Modules must not be null!");
|
||||
|
||||
return getAllModuleDependencies(modules) //
|
||||
.filter(it -> it.type == DependencyType.EVENT_LISTENER) //
|
||||
.map(ModuleDependency::getTarget) //
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all {@link EventType}s published by the module.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public List<EventType> getPublishedEvents() {
|
||||
return publishedEvents.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all types that are considered aggregate roots.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public List<JavaClass> getAggregateRoots() {
|
||||
|
||||
return entities.get().stream() //
|
||||
.map(it -> ArchitecturallyEvidentType.of(it, getSpringBeansInternal())) //
|
||||
.filter(ArchitecturallyEvidentType::isAggregateRoot) //
|
||||
.map(ArchitecturallyEvidentType::getType) //
|
||||
.flatMap(this::resolveModuleSuperTypes) //
|
||||
.distinct() //
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all types that are considered aggregate roots.
|
||||
*
|
||||
* @param modules must not be {@literal null}.
|
||||
* @return
|
||||
* @deprecated since 1.3, use {@link #getAggregateRoots()} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public List<JavaClass> getAggregateRoots(Modules modules) {
|
||||
|
||||
Assert.notNull(modules, "Modules must not be null!");
|
||||
|
||||
return getAggregateRoots();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all modules that contain types which the types of the current module depend on.
|
||||
*
|
||||
* @param modules must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public Stream<Module> getBootstrapDependencies(Modules modules) {
|
||||
|
||||
Assert.notNull(modules, "Modules must not be null!");
|
||||
|
||||
return getBootstrapDependencies(modules, DependencyDepth.IMMEDIATE);
|
||||
}
|
||||
|
||||
public Stream<Module> getBootstrapDependencies(Modules modules, DependencyDepth depth) {
|
||||
|
||||
Assert.notNull(modules, "Modules must not be null!");
|
||||
Assert.notNull(depth, "Dependency depth must not be null!");
|
||||
|
||||
return streamDependencies(modules, depth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all {@link JavaPackage} for the current module including the ones by its dependencies.
|
||||
*
|
||||
* @param modules must not be {@literal null}.
|
||||
* @param depth must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public Stream<JavaPackage> getBasePackages(Modules modules, DependencyDepth depth) {
|
||||
|
||||
Assert.notNull(modules, "Modules must not be null!");
|
||||
Assert.notNull(depth, "Dependency depth must not be null!");
|
||||
|
||||
Stream<Module> dependencies = streamDependencies(modules, depth);
|
||||
|
||||
return Stream.concat(Stream.of(this), dependencies) //
|
||||
.map(Module::getBasePackage);
|
||||
}
|
||||
|
||||
public List<SpringBean> getSpringBeans() {
|
||||
return getSpringBeansInternal().stream() //
|
||||
.map(it -> SpringBean.of(it, this)) //
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
Classes getSpringBeansInternal() {
|
||||
return springBeans.get();
|
||||
}
|
||||
|
||||
public boolean contains(JavaClass type) {
|
||||
return basePackage.contains(type);
|
||||
}
|
||||
|
||||
public boolean contains(@Nullable Class<?> type) {
|
||||
return type != null && getType(type.getName()).isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link JavaClass} for the given candidate simple of fully-qualified type name.
|
||||
*
|
||||
* @param candidate must not be {@literal null} or empty.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.1
|
||||
*/
|
||||
public Optional<JavaClass> getType(String candidate) {
|
||||
|
||||
Assert.hasText(candidate, "Candidate must not be null or emtpy!");
|
||||
|
||||
return basePackage.stream()
|
||||
.filter(hasSimpleOrFullyQualifiedName(candidate))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given {@link JavaClass} is exposed by the current module, i.e. whether it's part of any of the
|
||||
* module's named interfaces.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public boolean isExposed(JavaClass type) {
|
||||
|
||||
Assert.notNull(type, "Type must not be null!");
|
||||
|
||||
return namedInterfaces.stream().anyMatch(it -> it.contains(type));
|
||||
}
|
||||
|
||||
public void verifyDependencies(Modules modules) {
|
||||
detectDependencies(modules).throwIfPresent();
|
||||
}
|
||||
|
||||
public Violations detectDependencies(Modules modules) {
|
||||
|
||||
return getAllModuleDependencies(modules) //
|
||||
.map(it -> it.isValidDependencyWithin(modules)) //
|
||||
.reduce(Violations.NONE, Violations::and);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return toString(null);
|
||||
}
|
||||
|
||||
public String toString(@Nullable Modules modules) {
|
||||
|
||||
StringBuilder builder = new StringBuilder("## ").append(getDisplayName()).append(" ##\n");
|
||||
builder.append("> Logical name: ").append(getName()).append('\n');
|
||||
builder.append("> Base package: ").append(basePackage.getName()).append('\n');
|
||||
|
||||
if (namedInterfaces.hasExplicitInterfaces()) {
|
||||
|
||||
builder.append("> Named interfaces:\n");
|
||||
|
||||
namedInterfaces.forEach(it -> builder.append(" + ") //
|
||||
.append(it.toString()) //
|
||||
.append('\n'));
|
||||
}
|
||||
|
||||
if (modules != null) {
|
||||
|
||||
List<Module> dependencies = getBootstrapDependencies(modules).collect(Collectors.toList());
|
||||
|
||||
builder.append("> Direct module dependencies: ");
|
||||
builder.append(dependencies.isEmpty() ? "none"
|
||||
: dependencies.stream().map(Module::getName).collect(Collectors.joining(", ")));
|
||||
builder.append('\n');
|
||||
}
|
||||
|
||||
Classes beans = getSpringBeansInternal();
|
||||
|
||||
if (beans.isEmpty()) {
|
||||
|
||||
builder.append("> Spring beans: none\n");
|
||||
|
||||
} else {
|
||||
|
||||
builder.append("> Spring beans:\n");
|
||||
beans.forEach(it -> builder.append(" ") //
|
||||
.append(Classes.format(it, basePackage.getName()))//
|
||||
.append('\n'));
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all allowed module dependencies, either explicitly declared or defined as shared on the given
|
||||
* {@link Modules} instance.
|
||||
*
|
||||
* @param modules must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
List<Module> getAllowedDependencies(Modules modules) {
|
||||
|
||||
Assert.notNull(modules, "Modules must not be null!");
|
||||
|
||||
List<String> allowedDependencyNames = information.getAllowedDependencies();
|
||||
|
||||
if (allowedDependencyNames.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Stream<Module> explicitlyDeclaredModules = allowedDependencyNames.stream() //
|
||||
.map(modules::getModuleByName) //
|
||||
.flatMap(it -> it.map(Stream::of).orElse(Stream.empty()));
|
||||
|
||||
return Stream.concat(explicitlyDeclaredModules, modules.getSharedModules().stream()) //
|
||||
.distinct() //
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given module contains a type with the given simple or fully qualified name.
|
||||
*
|
||||
* @param candidate must not be {@literal null} or empty.
|
||||
* @return
|
||||
* @since 1.1
|
||||
*/
|
||||
boolean contains(String candidate) {
|
||||
|
||||
Assert.hasText(candidate, "Candidate must not be null or empty!");
|
||||
|
||||
return getType(candidate).isPresent();
|
||||
}
|
||||
|
||||
private List<EventType> findPublishedEvents() {
|
||||
|
||||
DescribedPredicate<JavaClass> isEvent = implement(JMoleculesTypes.DOMAIN_EVENT) //
|
||||
.or(isAnnotatedWith(JMoleculesTypes.AT_DOMAIN_EVENT));
|
||||
|
||||
return basePackage.that(isEvent).stream() //
|
||||
.map(EventType::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Stream} of all super types of the given one that are declared in the same module as well as the
|
||||
* type itself.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private Stream<JavaClass> resolveModuleSuperTypes(JavaClass type) {
|
||||
|
||||
Assert.notNull(type, "Type must not be null!");
|
||||
|
||||
return Stream.concat(//
|
||||
type.getAllRawSuperclasses().stream().filter(this::contains), //
|
||||
Stream.of(type));
|
||||
}
|
||||
|
||||
private Stream<ModuleDependency> getAllModuleDependencies(Modules modules) {
|
||||
|
||||
return basePackage.stream() //
|
||||
.flatMap(it -> getModuleDependenciesOf(it, modules));
|
||||
}
|
||||
|
||||
private Stream<Module> streamDependencies(Modules modules, DependencyDepth depth) {
|
||||
|
||||
switch (depth) {
|
||||
|
||||
case NONE:
|
||||
return Stream.empty();
|
||||
case IMMEDIATE:
|
||||
return getDirectModuleDependencies(modules);
|
||||
case ALL:
|
||||
default:
|
||||
return getDirectModuleDependencies(modules) //
|
||||
.flatMap(it -> Stream.concat(Stream.of(it), it.streamDependencies(modules, DependencyDepth.ALL))) //
|
||||
.distinct();
|
||||
}
|
||||
}
|
||||
|
||||
private Stream<Module> getDirectModuleDependencies(Modules modules) {
|
||||
|
||||
return getSpringBeansInternal().stream() //
|
||||
.flatMap(it -> ModuleDependency.fromType(it)) //
|
||||
.filter(it -> isDependencyToOtherModule(it.target, modules)) //
|
||||
.map(it -> modules.getModuleByType(it.target)) //
|
||||
.distinct() //
|
||||
.flatMap(it -> it.map(Stream::of).orElseGet(Stream::empty));
|
||||
}
|
||||
|
||||
private Stream<ModuleDependency> getModuleDependenciesOf(JavaClass type, Modules modules) {
|
||||
|
||||
Stream<ModuleDependency> injections = ModuleDependency.fromType(type) //
|
||||
.filter(it -> isDependencyToOtherModule(it.getTarget(), modules)); //
|
||||
|
||||
Stream<ModuleDependency> directDependencies = type.getDirectDependenciesFromSelf().stream() //
|
||||
.filter(it -> isDependencyToOtherModule(it.getTargetClass(), modules)) //
|
||||
.map(ModuleDependency::new);
|
||||
|
||||
return Stream.concat(injections, directDependencies).distinct();
|
||||
}
|
||||
|
||||
private boolean isDependencyToOtherModule(JavaClass dependency, Modules modules) {
|
||||
return modules.contains(dependency) && !contains(dependency);
|
||||
}
|
||||
|
||||
private Classes findEntities(JavaPackage source) {
|
||||
|
||||
return source.stream() //
|
||||
.map(it -> ArchitecturallyEvidentType.of(it, getSpringBeansInternal()))
|
||||
.filter(ArchitecturallyEvidentType::isEntity) //
|
||||
.map(ArchitecturallyEvidentType::getType).collect(toClasses());
|
||||
}
|
||||
|
||||
private static Classes filterSpringBeans(JavaPackage source) {
|
||||
|
||||
Map<Boolean, List<JavaClass>> collect = source.that(isConfiguration()).stream() //
|
||||
.flatMap(it -> it.getMethods().stream()) //
|
||||
.filter(SpringTypes::isAtBeanMethod) //
|
||||
.map(JavaMethod::getRawReturnType) //
|
||||
.collect(Collectors.groupingBy(it -> source.contains(it)));
|
||||
|
||||
Classes repositories = source.that(isSpringDataRepository());
|
||||
Classes coreComponents = source.that(not(INTERFACES).and(isComponent()));
|
||||
Classes configurationProperties = source.that(isConfigurationProperties());
|
||||
|
||||
return coreComponents //
|
||||
.and(repositories) //
|
||||
.and(configurationProperties) //
|
||||
.and(collect.getOrDefault(true, Collections.emptyList())) //
|
||||
.and(collect.getOrDefault(false, Collections.emptyList()));
|
||||
}
|
||||
|
||||
private static Predicate<JavaClass> hasSimpleOrFullyQualifiedName(String candidate) {
|
||||
return it -> it.getSimpleName().equals(candidate) || it.getFullName().equals(candidate);
|
||||
}
|
||||
|
||||
public enum DependencyDepth {
|
||||
|
||||
NONE,
|
||||
|
||||
IMMEDIATE,
|
||||
|
||||
ALL;
|
||||
}
|
||||
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
static class ModuleDependency {
|
||||
|
||||
private static final List<String> INJECTION_TYPES = Arrays.asList(//
|
||||
AT_AUTOWIRED, AT_RESOURCE, AT_INJECT);
|
||||
|
||||
private final @NonNull @Getter JavaClass origin, target;
|
||||
private final @NonNull String description;
|
||||
private final @NonNull DependencyType type;
|
||||
|
||||
ModuleDependency(Dependency dependency) {
|
||||
this(dependency.getOriginClass(), //
|
||||
dependency.getTargetClass(), //
|
||||
dependency.getDescription(), //
|
||||
DependencyType.forDependency(dependency));
|
||||
}
|
||||
|
||||
boolean hasType(DependencyType type) {
|
||||
return this.type.equals(type);
|
||||
}
|
||||
|
||||
Violations isValidDependencyWithin(Modules modules) {
|
||||
|
||||
Module originModule = getExistingModuleOf(origin, modules);
|
||||
Module targetModule = getExistingModuleOf(target, modules);
|
||||
|
||||
List<Module> allowedTargets = originModule.getAllowedDependencies(modules);
|
||||
Violations violations = Violations.NONE;
|
||||
|
||||
if (!allowedTargets.isEmpty() && !allowedTargets.contains(targetModule)) {
|
||||
|
||||
String allowedTargetsString = allowedTargets.stream() //
|
||||
.map(Module::getName) //
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
String message = String.format("Module '%s' depends on module '%s' via %s -> %s. Allowed target modules: %s.",
|
||||
originModule.getName(), targetModule.getName(), origin.getName(), target.getName(), allowedTargetsString);
|
||||
|
||||
violations = violations.and(new IllegalStateException(message));
|
||||
}
|
||||
|
||||
if (!targetModule.isExposed(target)) {
|
||||
|
||||
String violationText = String.format("Module '%s' depends on non-exposed type %s within module '%s'!",
|
||||
originModule.getName(), target.getName(), targetModule.getName());
|
||||
|
||||
violations = violations.and(new IllegalStateException(violationText + lineSeparator() + description));
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
Module getExistingModuleOf(JavaClass javaClass, Modules modules) {
|
||||
|
||||
Optional<Module> module = modules.getModuleByType(javaClass);
|
||||
|
||||
return module.orElseThrow(() -> new IllegalStateException(
|
||||
String.format("Origin/Target of a %s should always be within a module, but %s is not",
|
||||
getClass().getSimpleName(), javaClass.getName())));
|
||||
}
|
||||
|
||||
static ModuleDependency fromCodeUnitParameter(JavaCodeUnit codeUnit, JavaClass parameter) {
|
||||
|
||||
String description = createDescription(codeUnit, parameter, "parameter");
|
||||
|
||||
DependencyType type = DependencyType.forCodeUnit(codeUnit) //
|
||||
.or(() -> DependencyType.forParameter(parameter));
|
||||
|
||||
return new ModuleDependency(codeUnit.getOwner(), parameter, description, type);
|
||||
}
|
||||
|
||||
static ModuleDependency fromCodeUnitReturnType(JavaCodeUnit codeUnit) {
|
||||
|
||||
String description = createDescription(codeUnit, codeUnit.getRawReturnType(), "return type");
|
||||
|
||||
return new ModuleDependency(codeUnit.getOwner(), codeUnit.getRawReturnType(), description,
|
||||
DependencyType.DEFAULT);
|
||||
}
|
||||
|
||||
static Stream<ModuleDependency> fromType(JavaClass source) {
|
||||
return Stream.concat(Stream.concat(fromConstructorOf(source), fromMethodsOf(source)), fromFieldsOf(source));
|
||||
}
|
||||
|
||||
private static Stream<ModuleDependency> fromConstructorOf(JavaClass source) {
|
||||
|
||||
Set<JavaConstructor> constructors = source.getConstructors();
|
||||
|
||||
return constructors.stream() //
|
||||
.filter(it -> constructors.size() == 1 || isInjectionPoint(it)) //
|
||||
.flatMap(it -> it.getRawParameterTypes().stream() //
|
||||
.map(parameter -> new InjectionModuleDependency(source, parameter, it)));
|
||||
}
|
||||
|
||||
private static Stream<ModuleDependency> fromFieldsOf(JavaClass source) {
|
||||
|
||||
Stream<ModuleDependency> fieldInjections = source.getAllFields().stream() //
|
||||
.filter(ModuleDependency::isInjectionPoint) //
|
||||
.map(field -> new InjectionModuleDependency(source, field.getRawType(), field));
|
||||
|
||||
return fieldInjections;
|
||||
}
|
||||
|
||||
private static Stream<ModuleDependency> fromMethodsOf(JavaClass source) {
|
||||
|
||||
Set<JavaMethod> methods = source.getAllMethods().stream() //
|
||||
.filter(it -> !it.getOwner().isEquivalentTo(Object.class)) //
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
if (methods.isEmpty()) {
|
||||
return Stream.empty();
|
||||
}
|
||||
|
||||
Stream<ModuleDependency> returnTypes = methods.stream() //
|
||||
.filter(it -> !it.getRawReturnType().isPrimitive()) //
|
||||
.filter(it -> !it.getRawReturnType().getPackageName().startsWith("java")) //
|
||||
.map(it -> fromCodeUnitReturnType(it));
|
||||
|
||||
Set<JavaMethod> injectionMethods = methods.stream() //
|
||||
.filter(ModuleDependency::isInjectionPoint) //
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Stream<ModuleDependency> methodInjections = injectionMethods.stream() //
|
||||
.flatMap(it -> it.getRawParameterTypes().stream() //
|
||||
.map(parameter -> new InjectionModuleDependency(source, parameter, it)));
|
||||
|
||||
Stream<ModuleDependency> otherMethods = methods.stream() //
|
||||
.filter(it -> !injectionMethods.contains(it)) //
|
||||
.flatMap(it -> it.getRawParameterTypes().stream() //
|
||||
.map(parameter -> fromCodeUnitParameter(it, parameter)));
|
||||
|
||||
return Stream.concat(Stream.concat(methodInjections, otherMethods), returnTypes);
|
||||
}
|
||||
|
||||
static Stream<ModuleDependency> allFrom(JavaCodeUnit codeUnit) {
|
||||
|
||||
Stream<ModuleDependency> parameterDependencies = codeUnit.getRawParameterTypes()//
|
||||
.stream() //
|
||||
.map(it -> fromCodeUnitParameter(codeUnit, it));
|
||||
|
||||
Stream<ModuleDependency> returnType = Stream.of(fromCodeUnitReturnType(codeUnit));
|
||||
|
||||
return Stream.concat(parameterDependencies, returnType);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return type.format(FormatableJavaClass.of(origin), FormatableJavaClass.of(target));
|
||||
}
|
||||
|
||||
private static String createDescription(JavaMember codeUnit, JavaClass declaringElement,
|
||||
String declarationDescription) {
|
||||
|
||||
String type = declaringElement.getSimpleName();
|
||||
|
||||
String codeUnitDescription = JavaConstructor.class.isInstance(codeUnit) //
|
||||
? String.format("%s", declaringElement.getSimpleName()) //
|
||||
: String.format("%s.%s", declaringElement.getSimpleName(), codeUnit.getName());
|
||||
|
||||
if (JavaCodeUnit.class.isInstance(codeUnit)) {
|
||||
codeUnitDescription = String.format("%s(%s)", codeUnitDescription,
|
||||
JavaCodeUnit.class.cast(codeUnit).getRawParameterTypes().stream() //
|
||||
.map(JavaClass::getSimpleName) //
|
||||
.collect(Collectors.joining(", ")));
|
||||
}
|
||||
|
||||
String annotations = codeUnit.getAnnotations().stream() //
|
||||
.filter(it -> INJECTION_TYPES.contains(it.getRawType().getName())) //
|
||||
.map(it -> "@" + it.getRawType().getSimpleName()) //
|
||||
.collect(Collectors.joining(" ", "", " "));
|
||||
|
||||
annotations = StringUtils.hasText(annotations) ? annotations : "";
|
||||
|
||||
String declaration = declarationDescription + " " + annotations + codeUnitDescription;
|
||||
String location = SourceCodeLocation.of(codeUnit.getOwner(), 0).toString();
|
||||
|
||||
return String.format("%s declares %s in %s", type, declaration, location);
|
||||
}
|
||||
|
||||
private static boolean isInjectionPoint(JavaMember unit) {
|
||||
return INJECTION_TYPES.stream().anyMatch(type -> unit.isAnnotatedWith(type));
|
||||
}
|
||||
}
|
||||
|
||||
private static class InjectionModuleDependency extends ModuleDependency {
|
||||
|
||||
private final JavaMember member;
|
||||
private final boolean isConfigurationClass;
|
||||
|
||||
/**
|
||||
* @param origin
|
||||
* @param target
|
||||
* @param member
|
||||
*/
|
||||
public InjectionModuleDependency(JavaClass origin, JavaClass target, JavaMember member) {
|
||||
|
||||
super(origin, target, ModuleDependency.createDescription(member, origin, getDescriptionFor(member)),
|
||||
DependencyType.USES_COMPONENT);
|
||||
|
||||
this.member = member;
|
||||
this.isConfigurationClass = isConfiguration().apply(origin);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.Module.ModuleDependency#isValidDependencyWithin(org.moduliths.model.Modules)
|
||||
*/
|
||||
@Override
|
||||
Violations isValidDependencyWithin(Modules modules) {
|
||||
|
||||
Violations violations = super.isValidDependencyWithin(modules);
|
||||
|
||||
if (JavaField.class.isInstance(member) && !isConfigurationClass) {
|
||||
|
||||
Module module = getExistingModuleOf(member.getOwner(), modules);
|
||||
|
||||
violations = violations.and(new IllegalStateException(
|
||||
String.format("Module %s uses field injection in %s. Prefer constructor injection instead!",
|
||||
module.getDisplayName(), member.getFullName())));
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
private static String getDescriptionFor(JavaMember member) {
|
||||
|
||||
if (JavaConstructor.class.isInstance(member)) {
|
||||
return "constructor";
|
||||
} else if (JavaMethod.class.isInstance(member)) {
|
||||
return "injection method";
|
||||
} else if (JavaField.class.isInstance(member)) {
|
||||
return "injected field";
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(String.format("Invalid member type %s!", member.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
public enum DependencyType {
|
||||
|
||||
/**
|
||||
* Indicates that the module depends on the other one by a component dependency, i.e. that other module needs to be
|
||||
* bootstrapped to run the source module.
|
||||
*/
|
||||
USES_COMPONENT {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.Module.DependencyType#format(org.moduliths.model.FormatableJavaClass, org.moduliths.model.FormatableJavaClass)
|
||||
*/
|
||||
@Override
|
||||
public String format(FormatableJavaClass source, FormatableJavaClass target) {
|
||||
return String.format("Component %s using %s", source.getAbbreviatedFullName(), target.getAbbreviatedFullName());
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Indicates that the module refers to an entity of the other.
|
||||
*/
|
||||
ENTITY {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.Module.DependencyType#format(org.moduliths.model.FormatableJavaClass, org.moduliths.model.FormatableJavaClass)
|
||||
*/
|
||||
@Override
|
||||
public String format(FormatableJavaClass source, FormatableJavaClass target) {
|
||||
return String.format("Entity %s depending on %s", source.getAbbreviatedFullName(),
|
||||
target.getAbbreviatedFullName());
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Indicates that the module depends on the other by declaring an event listener for an event exposed by the other
|
||||
* module. Thus, the target module does not have to be bootstrapped to run the source one.
|
||||
*/
|
||||
EVENT_LISTENER {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.Module.DependencyType#format(org.moduliths.model.FormatableJavaClass, org.moduliths.model.FormatableJavaClass)
|
||||
*/
|
||||
@Override
|
||||
public String format(FormatableJavaClass source, FormatableJavaClass target) {
|
||||
return String.format("%s listening to events of type %s", source.getAbbreviatedFullName(),
|
||||
target.getAbbreviatedFullName());
|
||||
}
|
||||
},
|
||||
|
||||
DEFAULT {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.Module.DependencyType#or(com.tngtech.archunit.thirdparty.com.google.common.base.Supplier)
|
||||
*/
|
||||
@Override
|
||||
public DependencyType or(Supplier<DependencyType> supplier) {
|
||||
return supplier.get();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.Module.DependencyType#format(org.moduliths.model.FormatableJavaClass, org.moduliths.model.FormatableJavaClass)
|
||||
*/
|
||||
@Override
|
||||
public String format(FormatableJavaClass source, FormatableJavaClass target) {
|
||||
return String.format("%s depending on %s", source.getAbbreviatedFullName(), target.getAbbreviatedFullName());
|
||||
}
|
||||
};
|
||||
|
||||
public static DependencyType forParameter(JavaClass type) {
|
||||
return type.isAnnotatedWith("javax.persistence.Entity") ? ENTITY : DEFAULT;
|
||||
}
|
||||
|
||||
public static DependencyType forCodeUnit(JavaCodeUnit codeUnit) {
|
||||
return Types.isAnnotatedWith(SpringTypes.AT_EVENT_LISTENER).apply(codeUnit) //
|
||||
|| Types.isAnnotatedWith(JMoleculesTypes.AT_DOMAIN_EVENT_HANDLER).apply(codeUnit) //
|
||||
? EVENT_LISTENER
|
||||
: DEFAULT;
|
||||
}
|
||||
|
||||
public static DependencyType forDependency(Dependency dependency) {
|
||||
return forParameter(dependency.getTargetClass());
|
||||
}
|
||||
|
||||
public abstract String format(FormatableJavaClass source, FormatableJavaClass target);
|
||||
|
||||
public DependencyType or(Supplier<DependencyType> supplier) {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all {@link DependencyType}s except the given ones.
|
||||
*
|
||||
* @param types must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public static Stream<DependencyType> allBut(Collection<DependencyType> types) {
|
||||
|
||||
Assert.notNull(types, "Types must not be null!");
|
||||
|
||||
Predicate<DependencyType> isIncluded = types::contains;
|
||||
|
||||
return Arrays.stream(values()) //
|
||||
.filter(isIncluded.negate());
|
||||
}
|
||||
|
||||
public static Stream<DependencyType> allBut(Stream<DependencyType> types) {
|
||||
return allBut(types.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all {@link DependencyType}s except the given ones.
|
||||
*
|
||||
* @param types must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public static Stream<DependencyType> allBut(DependencyType... types) {
|
||||
|
||||
Assert.notNull(types, "Types must not be null!");
|
||||
|
||||
return allBut(Arrays.asList(types));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.moduliths.Module;
|
||||
import org.moduliths.model.Types.JMoleculesTypes;
|
||||
|
||||
/**
|
||||
* Default implementations of {@link ModuleDetectionStrategy}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @see ModuleDetectionStrategy#directSubPackage()
|
||||
* @see ModuleDetectionStrategy#explictlyAnnotated()
|
||||
*/
|
||||
enum ModuleDetectionStrategies implements ModuleDetectionStrategy {
|
||||
|
||||
DIRECT_SUB_PACKAGES {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ModuleDetection#getModuleBasePackages(org.moduliths.model.JavaPackage)
|
||||
*/
|
||||
@Override
|
||||
public Stream<JavaPackage> getModuleBasePackages(
|
||||
JavaPackage basePackage) {
|
||||
return basePackage.getDirectSubPackages().stream();
|
||||
}
|
||||
},
|
||||
|
||||
EXPLICITLY_ANNOTATED {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ModuleDetection#getModuleBasePackages(org.moduliths.model.JavaPackage)
|
||||
*/
|
||||
@Override
|
||||
public Stream<JavaPackage> getModuleBasePackages(JavaPackage basePackage) {
|
||||
|
||||
return Stream.of(Module.class, JMoleculesTypes.getModuleAnnotationTypeIfPresent())
|
||||
.filter(Objects::nonNull)
|
||||
.flatMap(basePackage::getSubPackagesAnnotatedWith);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.moduliths.Module;
|
||||
|
||||
/**
|
||||
* Strategy interface to customize which packages are considered module base packages.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public interface ModuleDetectionStrategy {
|
||||
|
||||
/**
|
||||
* Given the {@link JavaPackage} that Moduliths was initialized with, return the base packages for all modules in the
|
||||
* system.
|
||||
*
|
||||
* @param basePackage will never be {@literal null}.
|
||||
* @return must not be {@literal null}.
|
||||
*/
|
||||
Stream<JavaPackage> getModuleBasePackages(JavaPackage basePackage);
|
||||
|
||||
/**
|
||||
* A {@link ModuleDetectionStrategy} that considers all direct sub-packages of the Moduliths base package to be module
|
||||
* base packages.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
static ModuleDetectionStrategy directSubPackage() {
|
||||
return ModuleDetectionStrategies.DIRECT_SUB_PACKAGES;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link ModuleDetectionStrategy} that considers packages explicitly annotated with {@link Module} module base
|
||||
* packages.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
static ModuleDetectionStrategy explictlyAnnotated() {
|
||||
return ModuleDetectionStrategies.EXPLICITLY_ANNOTATED;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.moduliths.Module;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Abstraction for low-level module information. Used to support different annotations to configure metadata about a
|
||||
* module.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
interface ModuleInformation {
|
||||
|
||||
public static ModuleInformation of(JavaPackage javaPackage) {
|
||||
|
||||
if (ClassUtils.isPresent("org.jmolecules.ddd.annotation.Module", ModuleInformation.class.getClassLoader())
|
||||
&& MoleculesModule.supports(javaPackage)) {
|
||||
return new MoleculesModule(javaPackage);
|
||||
}
|
||||
|
||||
return new ModulithsModule(javaPackage);
|
||||
}
|
||||
|
||||
String getDisplayName();
|
||||
|
||||
List<String> getAllowedDependencies();
|
||||
|
||||
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
static abstract class AbstractModuleInformation implements ModuleInformation {
|
||||
|
||||
private final JavaPackage javaPackage;
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ModuleInformation#getName()
|
||||
*/
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return javaPackage.getName();
|
||||
}
|
||||
}
|
||||
|
||||
static class MoleculesModule extends AbstractModuleInformation {
|
||||
|
||||
private final Optional<org.jmolecules.ddd.annotation.Module> annotation;
|
||||
|
||||
public static boolean supports(JavaPackage javaPackage) {
|
||||
return javaPackage.getAnnotation(org.jmolecules.ddd.annotation.Module.class).isPresent();
|
||||
}
|
||||
|
||||
public MoleculesModule(JavaPackage javaPackage) {
|
||||
|
||||
super(javaPackage);
|
||||
|
||||
this.annotation = javaPackage.getAnnotation(org.jmolecules.ddd.annotation.Module.class);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ModuleInformation#getName()
|
||||
*/
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
|
||||
return annotation //
|
||||
.map(org.jmolecules.ddd.annotation.Module::name) //
|
||||
.filter(StringUtils::hasText)
|
||||
.orElseGet(() -> annotation //
|
||||
.map(org.jmolecules.ddd.annotation.Module::value) //
|
||||
.filter(StringUtils::hasText) //
|
||||
.orElseGet(super::getDisplayName));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ModuleInformation#getAllowedDependencies()
|
||||
*/
|
||||
@Override
|
||||
public List<String> getAllowedDependencies() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
static class ModulithsModule extends AbstractModuleInformation {
|
||||
|
||||
private final Optional<Module> annotation;
|
||||
|
||||
public static boolean supports(JavaPackage javaPackage) {
|
||||
return javaPackage.getAnnotation(Module.class).isPresent();
|
||||
}
|
||||
|
||||
public ModulithsModule(JavaPackage javaPackage) {
|
||||
|
||||
super(javaPackage);
|
||||
|
||||
this.annotation = javaPackage.getAnnotation(Module.class);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ModuleInformation.AbstractModuleInformation#getName()
|
||||
*/
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
|
||||
return annotation //
|
||||
.map(Module::displayName) //
|
||||
.filter(StringUtils::hasText) //
|
||||
.orElseGet(super::getDisplayName);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.ModuleInformation#getAllowedDependencies()
|
||||
*/
|
||||
@Override
|
||||
public List<String> getAllowedDependencies() {
|
||||
|
||||
return annotation //
|
||||
.map(it -> Arrays.stream(it.allowedDependencies())) //
|
||||
.orElse(Stream.empty()) //
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
}
|
||||
451
moduliths-core/src/main/java/org/moduliths/model/Modules.java
Normal file
451
moduliths-core/src/main/java/org/moduliths/model/Modules.java
Normal file
@@ -0,0 +1,451 @@
|
||||
/*
|
||||
* Copyright 2018-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import static com.tngtech.archunit.base.DescribedPredicate.*;
|
||||
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
|
||||
import static java.util.stream.Collectors.*;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.Value;
|
||||
import lombok.With;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jmolecules.archunit.JMoleculesDddRules;
|
||||
import org.moduliths.Modulith;
|
||||
import org.moduliths.Modulithic;
|
||||
import org.moduliths.model.Types.JMoleculesTypes;
|
||||
import org.springframework.core.io.support.SpringFactoriesLoader;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.tngtech.archunit.base.DescribedPredicate;
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
import com.tngtech.archunit.core.domain.JavaClasses;
|
||||
import com.tngtech.archunit.core.importer.ClassFileImporter;
|
||||
import com.tngtech.archunit.core.importer.ImportOption;
|
||||
import com.tngtech.archunit.lang.EvaluationResult;
|
||||
import com.tngtech.archunit.lang.FailureReport;
|
||||
import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
* @author Peter Gafert
|
||||
*/
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class Modules implements Iterable<Module> {
|
||||
|
||||
private static final Map<CacheKey, Modules> CACHE = new HashMap<>();
|
||||
|
||||
private static final ModuleDetectionStrategy DETECTION_STRATEGY;
|
||||
|
||||
static {
|
||||
|
||||
List<ModuleDetectionStrategy> loadFactories = SpringFactoriesLoader.loadFactories(ModuleDetectionStrategy.class,
|
||||
Modules.class.getClassLoader());
|
||||
|
||||
if (loadFactories.size() > 1) {
|
||||
|
||||
throw new IllegalStateException(
|
||||
String.format("Multiple module detection strategies configured. Only one supported! %s",
|
||||
loadFactories));
|
||||
}
|
||||
|
||||
DETECTION_STRATEGY = loadFactories.isEmpty() ? ModuleDetectionStrategies.DIRECT_SUB_PACKAGES : loadFactories.get(0);
|
||||
}
|
||||
|
||||
private final ModulithMetadata metadata;
|
||||
private final Map<String, Module> modules;
|
||||
private final JavaClasses allClasses;
|
||||
private final List<JavaPackage> rootPackages;
|
||||
private final @With(AccessLevel.PRIVATE) @Getter Set<Module> sharedModules;
|
||||
|
||||
private boolean verified;
|
||||
|
||||
private Modules(ModulithMetadata metadata, Collection<String> packages, DescribedPredicate<JavaClass> ignored,
|
||||
boolean useFullyQualifiedModuleNames) {
|
||||
|
||||
this.metadata = metadata;
|
||||
this.allClasses = new ClassFileImporter() //
|
||||
.withImportOption(new ImportOption.DoNotIncludeTests()) //
|
||||
.importPackages(packages) //
|
||||
.that(not(ignored));
|
||||
|
||||
Classes classes = Classes.of(allClasses);
|
||||
|
||||
this.modules = packages.stream() //
|
||||
.map(it -> JavaPackage.of(classes, it))
|
||||
.flatMap(DETECTION_STRATEGY::getModuleBasePackages) //
|
||||
.map(it -> new Module(it, useFullyQualifiedModuleNames)) //
|
||||
.collect(toMap(Module::getName, Function.identity()));
|
||||
|
||||
this.rootPackages = packages.stream() //
|
||||
.map(it -> JavaPackage.of(classes, it).toSingle()) //
|
||||
.collect(Collectors.toList());
|
||||
|
||||
this.sharedModules = Collections.emptySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link Modules} relative to the given modulith type. Will inspect the {@link Modulith} annotation on
|
||||
* the class given for advanced customizations of the module setup.
|
||||
*
|
||||
* @param modulithType must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public static Modules of(Class<?> modulithType) {
|
||||
return of(modulithType, alwaysFalse());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link Modules} relative to the given modulith type, a {@link ModuleDetectionStrategy} and a
|
||||
* {@link DescribedPredicate} which types and packages to ignore. Will inspect the {@link Modulith} and
|
||||
* {@link Modulithic} annotations on the class given for advanced customizations of the module setup.
|
||||
*
|
||||
* @param modulithType must not be {@literal null}.
|
||||
* @param detection must not be {@literal null}.
|
||||
* @param ignored must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public static Modules of(Class<?> modulithType, DescribedPredicate<JavaClass> ignored) {
|
||||
|
||||
CacheKey key = TypeKey.of(modulithType, ignored);
|
||||
|
||||
return CACHE.computeIfAbsent(key, it -> {
|
||||
|
||||
Assert.notNull(modulithType, "Modulith root type must not be null!");
|
||||
Assert.notNull(ignored, "Predicate to describe ignored types must not be null!");
|
||||
|
||||
return of(key);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link Modules} instance for the given package name.
|
||||
*
|
||||
* @param javaPackage must not be {@literal null} or empty.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.1
|
||||
*/
|
||||
public static Modules of(String javaPackage) {
|
||||
return of(javaPackage, alwaysFalse());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link Modules} instance for the given package name and ignored classes.
|
||||
*
|
||||
* @param javaPackage must not be {@literal null} or empty.
|
||||
* @param ignored must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.1
|
||||
*/
|
||||
public static Modules of(String javaPackage, DescribedPredicate<JavaClass> ignored) {
|
||||
|
||||
CacheKey key = PackageKey.of(javaPackage, ignored);
|
||||
|
||||
return CACHE.computeIfAbsent(key, it -> {
|
||||
|
||||
Assert.hasText(javaPackage, "Base package must not be null or empty!");
|
||||
Assert.notNull(ignored, "Predicate to describe ignored types must not be null!");
|
||||
|
||||
return of(key);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link Modules} instance for the given {@link CacheKey}.
|
||||
*
|
||||
* @param key must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
private static Modules of(CacheKey key) {
|
||||
|
||||
Assert.notNull(key, "Cache key must not be null!");
|
||||
|
||||
ModulithMetadata metadata = key.getMetadata();
|
||||
|
||||
Set<String> basePackages = new HashSet<>();
|
||||
basePackages.add(key.getBasePackage());
|
||||
basePackages.addAll(metadata.getAdditionalPackages());
|
||||
|
||||
Modules modules = new Modules(metadata, basePackages, key.getIgnored(),
|
||||
metadata.useFullyQualifiedModuleNames());
|
||||
|
||||
Set<Module> sharedModules = metadata.getSharedModuleNames() //
|
||||
.map(modules::getRequiredModule) //
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
return modules.withSharedModules(sharedModules);
|
||||
}
|
||||
|
||||
public Object getModulithSource() {
|
||||
return metadata.getModulithSource();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return
|
||||
* @deprecated since 1.1, as a {@link Modules} instance doesn't have to be created from a class in the first place.
|
||||
* For generic use, use {@link #getModulithSource()} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public Class<?> getModulithType() {
|
||||
|
||||
Object source = getModulithSource();
|
||||
|
||||
if (!Class.class.isInstance(source)) {
|
||||
throw new IllegalStateException(String.format("Moduliths not created from a type but %s!", source));
|
||||
}
|
||||
|
||||
return (Class<?>) source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given {@link JavaClass} is contained within the {@link Modules}.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public boolean contains(JavaClass type) {
|
||||
|
||||
Assert.notNull(type, "Type must not be null!");
|
||||
|
||||
return modules.values().stream() //
|
||||
.anyMatch(module -> module.contains(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given type is contained in one of the root packages (not including sub-packages) of the
|
||||
* modules.
|
||||
*
|
||||
* @param className must not be {@literal null} or empty.
|
||||
* @return
|
||||
*/
|
||||
public boolean withinRootPackages(String className) {
|
||||
|
||||
Assert.hasText(className, "Class name must not be null or empty!");
|
||||
|
||||
return rootPackages.stream().anyMatch(it -> it.contains(className));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Module} with the given name.
|
||||
*
|
||||
* @param name must not be {@literal null} or empty.
|
||||
* @return
|
||||
*/
|
||||
public Optional<Module> getModuleByName(String name) {
|
||||
|
||||
Assert.hasText(name, "Module name must not be null or empty!");
|
||||
|
||||
return Optional.ofNullable(modules.get(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the module that contains the given {@link JavaClass}.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public Optional<Module> getModuleByType(JavaClass type) {
|
||||
|
||||
Assert.notNull(type, "Type must not be null!");
|
||||
|
||||
return modules.values().stream() //
|
||||
.filter(it -> it.contains(type)) //
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Module} containing the type with the given simple or fully-qualified name.
|
||||
*
|
||||
* @param candidate must not be {@literal null} or empty.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.1
|
||||
*/
|
||||
public Optional<Module> getModuleByType(String candidate) {
|
||||
|
||||
Assert.hasText(candidate, "Candidate must not be null or empty!");
|
||||
|
||||
return modules.values().stream() //
|
||||
.filter(it -> it.contains(candidate)) //
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
public Optional<Module> getModuleForPackage(String name) {
|
||||
|
||||
return modules.values().stream() //
|
||||
.filter(it -> name.startsWith(it.getBasePackage().getName())) //
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
public void verify() {
|
||||
|
||||
if (verified) {
|
||||
return;
|
||||
}
|
||||
|
||||
Violations violations = detectViolations();
|
||||
|
||||
this.verified = true;
|
||||
|
||||
violations.throwIfPresent();
|
||||
}
|
||||
|
||||
public Violations detectViolations() {
|
||||
|
||||
Violations violations = rootPackages.stream() //
|
||||
.map(this::assertNoCyclesFor) //
|
||||
.flatMap(it -> it.getDetails().stream()) //
|
||||
.map(IllegalStateException::new) //
|
||||
.collect(Violations.toViolations());
|
||||
|
||||
if (JMoleculesTypes.areRulesPresent()) {
|
||||
|
||||
EvaluationResult result = JMoleculesDddRules.all().evaluate(allClasses);
|
||||
|
||||
for (String message : result.getFailureReport().getDetails()) {
|
||||
violations = violations.and(message);
|
||||
}
|
||||
}
|
||||
|
||||
return modules.values().stream() //
|
||||
.map(it -> it.detectDependencies(this)) //
|
||||
.reduce(violations, Violations::and);
|
||||
}
|
||||
|
||||
private FailureReport assertNoCyclesFor(JavaPackage rootPackage) {
|
||||
|
||||
EvaluationResult result = SlicesRuleDefinition.slices() //
|
||||
.matching(rootPackage.getName().concat(".(*)..")) //
|
||||
.should().beFreeOfCycles() //
|
||||
.evaluate(allClasses.that(resideInAPackage(rootPackage.getName().concat(".."))));
|
||||
|
||||
return result.getFailureReport();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all {@link Module}s.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public Stream<Module> stream() {
|
||||
return modules.values().stream();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the system name if defined.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Optional<String> getSystemName() {
|
||||
return metadata.getSystemName();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Iterable#iterator()
|
||||
*/
|
||||
@Override
|
||||
public Iterator<Module> iterator() {
|
||||
return modules.values().iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the module with the given name rejecting invalid module names.
|
||||
*
|
||||
* @param moduleName must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private Module getRequiredModule(String moduleName) {
|
||||
|
||||
Module module = modules.get(moduleName);
|
||||
|
||||
if (module == null) {
|
||||
throw new IllegalArgumentException(String.format("Module %s does not exist!", moduleName));
|
||||
}
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
public static class Filters {
|
||||
|
||||
public static DescribedPredicate<JavaClass> withoutModules(String... names) {
|
||||
|
||||
return Arrays.stream(names) //
|
||||
.map(it -> withoutModule(it)) //
|
||||
.reduce(DescribedPredicate.alwaysFalse(), DescribedPredicate::or, (__, right) -> right);
|
||||
}
|
||||
|
||||
public static DescribedPredicate<JavaClass> withoutModule(String name) {
|
||||
return resideInAPackage("..".concat(name).concat(".."));
|
||||
}
|
||||
}
|
||||
|
||||
private static interface CacheKey {
|
||||
|
||||
String getBasePackage();
|
||||
|
||||
DescribedPredicate<JavaClass> getIgnored();
|
||||
|
||||
ModulithMetadata getMetadata();
|
||||
}
|
||||
|
||||
@Value(staticConstructor = "of")
|
||||
private static final class TypeKey implements CacheKey {
|
||||
|
||||
Class<?> type;
|
||||
DescribedPredicate<JavaClass> ignored;
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.Modules.CacheKey#getBasePackage()
|
||||
*/
|
||||
@Override
|
||||
public String getBasePackage() {
|
||||
return type.getPackage().getName();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.Modules.CacheKey#getMetadata()
|
||||
*/
|
||||
@Override
|
||||
public ModulithMetadata getMetadata() {
|
||||
return ModulithMetadata.of(type);
|
||||
}
|
||||
}
|
||||
|
||||
@Value(staticConstructor = "of")
|
||||
private static final class PackageKey implements CacheKey {
|
||||
|
||||
String basePackage;
|
||||
DescribedPredicate<JavaClass> ignored;
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.Modules.CacheKey#getMetadata()
|
||||
*/
|
||||
@Override
|
||||
public ModulithMetadata getMetadata() {
|
||||
return ModulithMetadata.of(basePackage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2019-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.moduliths.Modulith;
|
||||
import org.moduliths.Modulithic;
|
||||
import org.moduliths.model.Types.SpringTypes;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
interface ModulithMetadata {
|
||||
|
||||
static final String ANNOTATION_MISSING = "Modules can only be retrieved from a root type, but %s is not annotated with either @%s, @%s or @%s!";
|
||||
|
||||
/**
|
||||
* Creates a new {@link ModulithMetadata} for the given annotated type. Expecteds the type either be annotated with
|
||||
* {@link Modulith}, {@link Modulithic} or {@link SpringBootApplication}.
|
||||
*
|
||||
* @param annotated must not be {@literal null}.
|
||||
* @return
|
||||
* @throws IllegalArgumentException in case none of the above mentioned annotations is present on the given type.
|
||||
*/
|
||||
public static ModulithMetadata of(Class<?> annotated) {
|
||||
|
||||
Assert.notNull(annotated, "Annotated type must not be null!");
|
||||
|
||||
Supplier<IllegalArgumentException> exception = () -> new IllegalArgumentException(
|
||||
String.format(ANNOTATION_MISSING, annotated.getSimpleName(), Modulith.class.getSimpleName(),
|
||||
Modulithic.class.getSimpleName(), SpringTypes.AT_SPRING_BOOT_APPLICATION));
|
||||
|
||||
Supplier<ModulithMetadata> withDefaults = () -> DefaultModulithMetadata.of(annotated).orElseThrow(exception);
|
||||
|
||||
return AnnotationModulithMetadata.of(annotated).orElseGet(withDefaults);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ModulithMetadata} instance for the given package.
|
||||
*
|
||||
* @param javaPackage must not be {@literal null} or empty.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.1
|
||||
*/
|
||||
public static ModulithMetadata of(String javaPackage) {
|
||||
return DefaultModulithMetadata.of(javaPackage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the source of the Moduliths setup. Either a type or a package.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.1
|
||||
*/
|
||||
Object getModulithSource();
|
||||
|
||||
/**
|
||||
* Returns the names of the packages that are supposed to be considered modulith base packages, i.e. for which to
|
||||
* consider all direct sub-packages modules by default.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
List<String> getAdditionalPackages();
|
||||
|
||||
/**
|
||||
* Whether to use fully-qualified module names, i.e. rather use the fully-qualified package name instead of the local
|
||||
* one.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
boolean useFullyQualifiedModuleNames();
|
||||
|
||||
/**
|
||||
* Returns the name of shared modules, i.e. modules that are supposed to always be included in bootstraps.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
Stream<String> getSharedModuleNames();
|
||||
|
||||
/**
|
||||
* Returns the name of the system.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
Optional<String> getSystemName();
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.tngtech.archunit.base.DescribedPredicate;
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
import com.tngtech.archunit.core.domain.JavaClass.Predicates;
|
||||
import com.tngtech.archunit.core.domain.JavaModifier;
|
||||
import com.tngtech.archunit.core.domain.properties.HasModifiers;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public abstract class NamedInterface implements Iterable<JavaClass> {
|
||||
|
||||
private static final String UNNAMED_NAME = "<<UNNAMED>>";
|
||||
private static final String PACKAGE_INFO_NAME = "package-info";
|
||||
|
||||
protected final @Getter String name;
|
||||
|
||||
static NamedInterface unnamed(JavaPackage javaPackage) {
|
||||
return new PackageBasedNamedInterface(UNNAMED_NAME, javaPackage);
|
||||
}
|
||||
|
||||
public static List<PackageBasedNamedInterface> of(JavaPackage javaPackage) {
|
||||
|
||||
String[] name = javaPackage.getAnnotation(org.moduliths.NamedInterface.class) //
|
||||
.map(it -> it.value()) //
|
||||
.orElseThrow(() -> new IllegalArgumentException(
|
||||
String.format("Couldn't find NamedInterface annotation on package %s!", javaPackage)));
|
||||
|
||||
return Arrays.stream(name) //
|
||||
.map(it -> new PackageBasedNamedInterface(it, javaPackage)) //
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static TypeBasedNamedInterface of(String name, Classes classes, JavaPackage basePackage) {
|
||||
return new TypeBasedNamedInterface(name, classes, basePackage);
|
||||
}
|
||||
|
||||
public boolean isUnnamed() {
|
||||
return name.equals(UNNAMED_NAME);
|
||||
}
|
||||
|
||||
public boolean contains(JavaClass type) {
|
||||
return getClasses().contains(type);
|
||||
}
|
||||
|
||||
public boolean contains(Class<?> type) {
|
||||
return !getClasses().that(Predicates.equivalentTo(type)).isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given {@link NamedInterface} has the same name as the current one.
|
||||
*
|
||||
* @param other
|
||||
* @return
|
||||
*/
|
||||
boolean hasSameNameAs(NamedInterface other) {
|
||||
return this.name.equals(other.name);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Iterable#iterator()
|
||||
*/
|
||||
@Override
|
||||
public Iterator<JavaClass> iterator() {
|
||||
return getClasses().iterator();
|
||||
}
|
||||
|
||||
protected abstract Classes getClasses();
|
||||
|
||||
public abstract NamedInterface merge(TypeBasedNamedInterface other);
|
||||
|
||||
static class PackageBasedNamedInterface extends NamedInterface {
|
||||
|
||||
private final @Getter Classes classes;
|
||||
private final JavaPackage javaPackage;
|
||||
|
||||
public PackageBasedNamedInterface(String name, JavaPackage pkg) {
|
||||
|
||||
super(name);
|
||||
|
||||
Assert.notNull(pkg, "Package must not be null!");
|
||||
Assert.hasText(name, "Package name must not be null or empty!");
|
||||
|
||||
this.classes = pkg.toSingle().getClasses() //
|
||||
.that(HasModifiers.Predicates.modifier(JavaModifier.PUBLIC)) //
|
||||
.that(DescribedPredicate.not(JavaClass.Predicates.simpleName(PACKAGE_INFO_NAME)));
|
||||
|
||||
this.javaPackage = pkg;
|
||||
}
|
||||
|
||||
private PackageBasedNamedInterface(String name, Classes classes, JavaPackage pkg) {
|
||||
|
||||
super(name);
|
||||
this.classes = classes;
|
||||
this.javaPackage = pkg;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.NamedInterface#merge(org.moduliths.model.NamedInterface.TypeBasedNamedInterface)
|
||||
*/
|
||||
@Override
|
||||
public NamedInterface merge(TypeBasedNamedInterface other) {
|
||||
return new PackageBasedNamedInterface(name, classes.and(other.classes), javaPackage);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.NamedInterface#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s - Public types residing in %s:\n%s\n", name, javaPackage.getName(),
|
||||
classes.format(javaPackage.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
static class TypeBasedNamedInterface extends NamedInterface {
|
||||
|
||||
private final @Getter Classes classes;
|
||||
private final JavaPackage pkg;
|
||||
|
||||
public TypeBasedNamedInterface(String name, Classes types, JavaPackage pkg) {
|
||||
super(name);
|
||||
|
||||
this.classes = types;
|
||||
this.pkg = pkg;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.NamedInterface#merge(org.moduliths.model.NamedInterface.TypeBasedNamedInterface)
|
||||
*/
|
||||
@Override
|
||||
public NamedInterface merge(TypeBasedNamedInterface other) {
|
||||
return new TypeBasedNamedInterface(name, classes.and(other.classes), pkg);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.model.NamedInterface#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s - Types underneath base package %s:\n%s\n", name, pkg.getName(),
|
||||
classes.format(pkg.getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.moduliths.model.NamedInterface.TypeBasedNamedInterface;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class NamedInterfaces implements Iterable<NamedInterface> {
|
||||
|
||||
public static final NamedInterfaces NONE = new NamedInterfaces(Collections.emptyList());
|
||||
|
||||
private final List<NamedInterface> namedInterfaces;
|
||||
|
||||
public static NamedInterfaces discoverNamedInterfaces(JavaPackage basePackage) {
|
||||
|
||||
return NamedInterfaces.ofAnnotatedPackages(basePackage) //
|
||||
.and(NamedInterfaces.ofAnnotatedTypes(basePackage)) //
|
||||
.orUnnamed(basePackage);
|
||||
}
|
||||
|
||||
public static NamedInterfaces of(List<NamedInterface> interfaces) {
|
||||
return interfaces.isEmpty() ? NONE : new NamedInterfaces(interfaces);
|
||||
}
|
||||
|
||||
static NamedInterfaces ofAnnotatedPackages(JavaPackage basePackage) {
|
||||
|
||||
return basePackage //
|
||||
.getSubPackagesAnnotatedWith(org.moduliths.NamedInterface.class) //
|
||||
.flatMap(it -> NamedInterface.of(it).stream()) //
|
||||
.collect(Collectors.collectingAndThen(Collectors.toList(), NamedInterfaces::of));
|
||||
}
|
||||
|
||||
private static List<TypeBasedNamedInterface> ofAnnotatedTypes(JavaPackage basePackage) {
|
||||
|
||||
MultiValueMap<String, JavaClass> mappings = new LinkedMultiValueMap<>();
|
||||
|
||||
basePackage.stream() //
|
||||
.filter(it -> !JavaPackage.isPackageInfoType(it)) //
|
||||
.forEach(it -> {
|
||||
|
||||
if (!it.isAnnotatedWith(org.moduliths.NamedInterface.class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
org.moduliths.NamedInterface annotation = it
|
||||
.getAnnotationOfType(org.moduliths.NamedInterface.class);
|
||||
|
||||
for (String name : annotation.value()) {
|
||||
mappings.add(name, it);
|
||||
}
|
||||
});
|
||||
|
||||
return mappings.entrySet().stream() //
|
||||
.map(entry -> NamedInterface.of(entry.getKey(), Classes.of(entry.getValue()), basePackage)) //
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public boolean hasExplicitInterfaces() {
|
||||
return namedInterfaces.size() > 1 || !namedInterfaces.get(0).isUnnamed();
|
||||
}
|
||||
|
||||
public Stream<NamedInterface> stream() {
|
||||
return namedInterfaces.stream();
|
||||
}
|
||||
|
||||
public NamedInterfaces and(List<TypeBasedNamedInterface> others) {
|
||||
|
||||
List<NamedInterface> namedInterfaces = new ArrayList<>();
|
||||
List<NamedInterface> unmergedInterface = this.namedInterfaces;
|
||||
|
||||
for (TypeBasedNamedInterface candidate : others) {
|
||||
|
||||
Optional<NamedInterface> existing = namedInterfaces.stream() //
|
||||
.filter(it -> it.hasSameNameAs(candidate)) //
|
||||
.findFirst();
|
||||
|
||||
// Merge existing with new and add to result
|
||||
existing.ifPresent(it -> {
|
||||
namedInterfaces.add(it.merge(candidate));
|
||||
namedInterfaces.add(it);
|
||||
unmergedInterface.remove(it);
|
||||
});
|
||||
|
||||
// Simply add candidate
|
||||
if (!existing.isPresent()) {
|
||||
namedInterfaces.add(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
namedInterfaces.addAll(unmergedInterface);
|
||||
|
||||
return new NamedInterfaces(namedInterfaces);
|
||||
}
|
||||
|
||||
public NamedInterfaces orUnnamed(JavaPackage basePackage) {
|
||||
return namedInterfaces.isEmpty() //
|
||||
? of(Collections.singletonList(NamedInterface.unnamed(basePackage))) //
|
||||
: this;
|
||||
}
|
||||
|
||||
public Optional<NamedInterface> getByName(String name) {
|
||||
return namedInterfaces.stream().filter(it -> it.getName().equals(name)).findFirst();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Iterable#iterator()
|
||||
*/
|
||||
@Override
|
||||
public Iterator<NamedInterface> iterator() {
|
||||
return namedInterfaces.iterator();
|
||||
}
|
||||
}
|
||||
33
moduliths-core/src/main/java/org/moduliths/model/Source.java
Normal file
33
moduliths-core/src/main/java/org/moduliths/model/Source.java
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
/**
|
||||
* A {@link Source} of some type, bean definition etc. Essentially describes the origin of that bean, event etc.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 1.1
|
||||
*/
|
||||
public interface Source {
|
||||
|
||||
/**
|
||||
* Renders the source in human readable way.
|
||||
*
|
||||
* @param module must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
String toString(Module module);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
|
||||
/**
|
||||
* A Spring bean type.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor(staticName = "of", access = AccessLevel.PACKAGE)
|
||||
public class SpringBean {
|
||||
|
||||
private final @Getter JavaClass type;
|
||||
private final Module module;
|
||||
|
||||
/**
|
||||
* Returns the fully-qualified name of the Spring bean type.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getFullyQualifiedTypeName() {
|
||||
return type.getFullName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all interfaces implemented by the bean that are part of the same module.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public List<JavaClass> getInterfacesWithinModule() {
|
||||
|
||||
return type.getRawInterfaces().stream() //
|
||||
.filter(module::contains) //
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public boolean isAnnotatedWith(Class<?> type) {
|
||||
return Types.isAnnotatedWith(type).apply(this.type);
|
||||
}
|
||||
|
||||
public ArchitecturallyEvidentType toArchitecturallyEvidentType() {
|
||||
return ArchitecturallyEvidentType.of(type, module.getSpringBeansInternal());
|
||||
}
|
||||
}
|
||||
158
moduliths-core/src/main/java/org/moduliths/model/Types.java
Normal file
158
moduliths-core/src/main/java/org/moduliths/model/Types.java
Normal file
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import com.tngtech.archunit.base.DescribedPredicate;
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
import com.tngtech.archunit.core.domain.JavaMethod;
|
||||
import com.tngtech.archunit.core.domain.properties.CanBeAnnotated;
|
||||
import com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@UtilityClass
|
||||
class Types {
|
||||
|
||||
@Nullable
|
||||
@SuppressWarnings("unchecked")
|
||||
<T> Class<T> loadIfPresent(String name) {
|
||||
|
||||
ClassLoader loader = Types.class.getClassLoader();
|
||||
|
||||
return ClassUtils.isPresent(name, loader) ? (Class<T>) ClassUtils.resolveClassName(name, loader) : null;
|
||||
}
|
||||
|
||||
static class JMoleculesTypes {
|
||||
|
||||
private static final String BASE_PACKAGE = "org.jmolecules";
|
||||
private static final String ANNOTATION_PACKAGE = BASE_PACKAGE + ".ddd.annotation";
|
||||
private static final String AT_ENTITY = ANNOTATION_PACKAGE + ".Entity";
|
||||
private static final String ARCHUNIT_RULES = BASE_PACKAGE + ".archunit.JMoleculesDddRules";
|
||||
private static final String MODULE = ANNOTATION_PACKAGE + ".Module";
|
||||
|
||||
static final String AT_DOMAIN_EVENT_HANDLER = BASE_PACKAGE + ".event.annotation.DomainEventHandler";
|
||||
static final String AT_DOMAIN_EVENT = BASE_PACKAGE + ".event.annotation.DomainEvent";
|
||||
static final String DOMAIN_EVENT = BASE_PACKAGE + ".event.types.DomainEvent";
|
||||
|
||||
public static boolean isPresent() {
|
||||
return ClassUtils.isPresent(AT_ENTITY, JMoleculesTypes.class.getClassLoader());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Class<? extends Annotation> getModuleAnnotationTypeIfPresent() {
|
||||
|
||||
try {
|
||||
return isPresent()
|
||||
? (Class<? extends Annotation>) ClassUtils.forName(MODULE, JMoleculesTypes.class.getClassLoader())
|
||||
: null;
|
||||
} catch (Exception o_O) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean areRulesPresent() {
|
||||
return ClassUtils.isPresent(ARCHUNIT_RULES, JMoleculesTypes.class.getClassLoader());
|
||||
}
|
||||
}
|
||||
|
||||
@UtilityClass
|
||||
static class JavaXTypes {
|
||||
|
||||
private static final String BASE_PACKAGE = "javax";
|
||||
|
||||
static final String AT_ENTITY = BASE_PACKAGE + ".persistence.Entity";
|
||||
static final String AT_INJECT = BASE_PACKAGE + ".inject.Inject";
|
||||
static final String AT_RESOURCE = BASE_PACKAGE + ".annotation.Resource";
|
||||
|
||||
static DescribedPredicate<? super JavaClass> isJpaEntity() {
|
||||
return isAnnotatedWith(AT_ENTITY);
|
||||
}
|
||||
}
|
||||
|
||||
@UtilityClass
|
||||
static class SpringTypes {
|
||||
|
||||
private static final String BASE_PACKAGE = "org.springframework";
|
||||
|
||||
static final String APPLICATION_LISTENER = BASE_PACKAGE + ".context.ApplicationListener";
|
||||
static final String AT_AUTOWIRED = BASE_PACKAGE + ".beans.factory.annotation.Autowired";
|
||||
static final String AT_ASYNC = BASE_PACKAGE + ".scheduling.annotation.Async";
|
||||
static final String AT_BEAN = BASE_PACKAGE + ".context.annotation.Bean";
|
||||
static final String AT_COMPONENT = BASE_PACKAGE + ".stereotype.Component";
|
||||
static final String AT_CONFIGURATION = BASE_PACKAGE + ".context.annotation.Configuration";
|
||||
static final String AT_CONTROLLER = BASE_PACKAGE + ".stereotype.Controller";
|
||||
static final String AT_EVENT_LISTENER = BASE_PACKAGE + ".context.event.EventListener";
|
||||
static final String AT_REPOSITORY = BASE_PACKAGE + ".stereotype.Repository";
|
||||
static final String AT_SERVICE = BASE_PACKAGE + ".stereotype.Service";
|
||||
static final String AT_SPRING_BOOT_APPLICATION = BASE_PACKAGE + ".boot.autoconfigure.SpringBootApplication";
|
||||
static final String AT_TX_EVENT_LISTENER = BASE_PACKAGE + ".transaction.event.TransactionalEventListener";
|
||||
static final String AT_CONFIGURATION_PROPERTIES = BASE_PACKAGE + ".boot.context.properties.ConfigurationProperties";
|
||||
|
||||
static DescribedPredicate<? super JavaClass> isConfiguration() {
|
||||
return isAnnotatedWith(AT_CONFIGURATION);
|
||||
}
|
||||
|
||||
static DescribedPredicate<? super JavaClass> isComponent() {
|
||||
return isAnnotatedWith(AT_COMPONENT);
|
||||
}
|
||||
|
||||
static DescribedPredicate<? super JavaClass> isConfigurationProperties() {
|
||||
return isAnnotatedWith(AT_CONFIGURATION_PROPERTIES);
|
||||
}
|
||||
|
||||
static boolean isAtBeanMethod(JavaMethod method) {
|
||||
return isAnnotatedWith(SpringTypes.AT_BEAN).apply(method);
|
||||
}
|
||||
}
|
||||
|
||||
@UtilityClass
|
||||
static class SpringDataTypes {
|
||||
|
||||
private static final String BASE_PACKAGE = SpringTypes.BASE_PACKAGE + ".data";
|
||||
|
||||
static final String REPOSITORY = BASE_PACKAGE + ".repository.Repository";
|
||||
static final String AT_REPOSITORY_DEFINITION = BASE_PACKAGE + ".repository.RepositoryDefinition";
|
||||
|
||||
static boolean isPresent() {
|
||||
return ClassUtils.isPresent(REPOSITORY, SpringDataTypes.class.getClassLoader());
|
||||
}
|
||||
|
||||
static DescribedPredicate<JavaClass> isSpringDataRepository() {
|
||||
return assignableTo(SpringDataTypes.REPOSITORY) //
|
||||
.or(isAnnotatedWith(SpringDataTypes.AT_REPOSITORY_DEFINITION));
|
||||
}
|
||||
}
|
||||
|
||||
DescribedPredicate<CanBeAnnotated> isAnnotatedWith(Class<?> type) {
|
||||
return isAnnotatedWith(type.getName());
|
||||
}
|
||||
|
||||
DescribedPredicate<CanBeAnnotated> isAnnotatedWith(String type) {
|
||||
return Predicates.annotatedWith(type) //
|
||||
.or(Predicates.metaAnnotatedWith(type));
|
||||
}
|
||||
}
|
||||
122
moduliths-core/src/main/java/org/moduliths/model/Violations.java
Normal file
122
moduliths-core/src/main/java/org/moduliths/model/Violations.java
Normal file
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collector;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Value type to gather and report architectural violations.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@RequiredArgsConstructor(staticName = "of", access = AccessLevel.PRIVATE)
|
||||
public class Violations extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 6863781504675034691L;
|
||||
|
||||
public static Violations NONE = new Violations(Collections.emptyList());
|
||||
|
||||
private final List<RuntimeException> exceptions;
|
||||
|
||||
/**
|
||||
* A {@link Collector} to turn a {@link Stream} of {@link RuntimeException}s into a {@link Violations} instance.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
static Collector<RuntimeException, ?, Violations> toViolations() {
|
||||
return Collectors.collectingAndThen(Collectors.toList(), Violations::of);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Throwable#getMessage()
|
||||
*/
|
||||
@Override
|
||||
public String getMessage() {
|
||||
|
||||
return exceptions.stream() //
|
||||
.map(RuntimeException::getMessage) //
|
||||
.collect(Collectors.joining("\n- ", "- ", ""));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether there are violations available.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean hasViolations() {
|
||||
return !exceptions.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws itself in case it's not an empty instance.
|
||||
*/
|
||||
public void throwIfPresent() {
|
||||
|
||||
if (hasViolations()) {
|
||||
throw this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link Violations} with the given {@link RuntimeException} added to the current ones?
|
||||
*
|
||||
* @param exception must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
Violations and(RuntimeException exception) {
|
||||
|
||||
Assert.notNull(exception, "Exception must not be null!");
|
||||
|
||||
List<RuntimeException> newExceptions = new ArrayList<>(exceptions.size() + 1);
|
||||
newExceptions.addAll(exceptions);
|
||||
newExceptions.add(exception);
|
||||
|
||||
return new Violations(newExceptions);
|
||||
}
|
||||
|
||||
Violations and(Violations other) {
|
||||
|
||||
List<RuntimeException> newExceptions = new ArrayList<>(exceptions.size() + other.exceptions.size());
|
||||
newExceptions.addAll(exceptions);
|
||||
newExceptions.addAll(other.exceptions);
|
||||
|
||||
return new Violations(newExceptions);
|
||||
}
|
||||
|
||||
Violations and(String violation) {
|
||||
return and(new ArchitecturalViolation(violation));
|
||||
}
|
||||
|
||||
private static class ArchitecturalViolation extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 3587887036508024142L;
|
||||
|
||||
public ArchitecturalViolation(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
@org.springframework.lang.NonNullApi
|
||||
package org.moduliths.model;
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.acme.withatbean;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@Configuration
|
||||
public class SampleConfiguration {
|
||||
|
||||
@Bean
|
||||
DataSource dataSource() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.acme.withatbean;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public class TestEvents {
|
||||
|
||||
/**
|
||||
* Method calling a factory method.
|
||||
*/
|
||||
public void method() {
|
||||
JMoleculesAnnotated.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method calling a constructor.
|
||||
*/
|
||||
public void constructorCall() {
|
||||
new JMoleculesAnnotated();
|
||||
}
|
||||
|
||||
// jMolecules
|
||||
|
||||
@org.jmolecules.event.annotation.DomainEvent
|
||||
public static class JMoleculesAnnotated {
|
||||
public static JMoleculesAnnotated of() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class JMoleculesImplementing implements org.jmolecules.event.types.DomainEvent {}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
@org.jmolecules.ddd.annotation.Module
|
||||
package jmolecules;
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.moduliths.Modulithic;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link AnnotationModulithMetadata}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
class AnnotationModulithMetadataUnitTest {
|
||||
|
||||
@Test
|
||||
void findsCustomizationsOnClass() {
|
||||
|
||||
assertThat(AnnotationModulithMetadata.of(Sample.class)).hasValueSatisfying(it -> {
|
||||
assertThat(it.useFullyQualifiedModuleNames()).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void findsCustomizationsOnClassForMetaAnnotationUsage() {
|
||||
|
||||
assertThat(AnnotationModulithMetadata.of(MetaSample.class)).hasValueSatisfying(it -> {
|
||||
assertThat(it.useFullyQualifiedModuleNames()).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@Modulithic(useFullyQualifiedModuleNames = true)
|
||||
static class Sample {}
|
||||
|
||||
@Intermediate
|
||||
static class MetaSample {}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Modulithic(useFullyQualifiedModuleNames = true)
|
||||
@interface Intermediate {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
|
||||
import org.jmolecules.event.annotation.DomainEventHandler;
|
||||
import org.junit.jupiter.api.DynamicTest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestFactory;
|
||||
import org.moduliths.model.ArchitecturallyEvidentType.SpringAwareArchitecturallyEvidentType;
|
||||
import org.moduliths.model.ArchitecturallyEvidentType.SpringDataAwareArchitecturallyEvidentType;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ArchitecturallyEvidentType}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
class ArchitecturallyEvidentTypeUnitTest {
|
||||
|
||||
Classes classes = TestUtils.getClasses();
|
||||
JavaClass self = classes.getRequiredClass(ArchitecturallyEvidentTypeUnitTest.class);
|
||||
|
||||
@Test
|
||||
void abbreviatesFullyQualifiedTypeName() {
|
||||
|
||||
ArchitecturallyEvidentType type = ArchitecturallyEvidentType.of(self, classes);
|
||||
|
||||
assertThat(type.getAbbreviatedFullName()).isEqualTo("o.m.m.ArchitecturallyEvidentTypeUnitTest");
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotConsiderArbitraryTypeAStereotype() {
|
||||
|
||||
ArchitecturallyEvidentType type = ArchitecturallyEvidentType.of(self, classes);
|
||||
|
||||
assertThat(type.isEntity()).isFalse();
|
||||
assertThat(type.isAggregateRoot()).isFalse();
|
||||
assertThat(type.isRepository()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectsSpringAnnotatedRepositories() {
|
||||
|
||||
ArchitecturallyEvidentType type = new SpringAwareArchitecturallyEvidentType(
|
||||
classes.getRequiredClass(SpringRepository.class));
|
||||
|
||||
assertThat(type.isRepository()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotConsiderEntityAggregateRoot() {
|
||||
|
||||
ArchitecturallyEvidentType type = new SpringAwareArchitecturallyEvidentType(
|
||||
classes.getRequiredClass(SampleEntity.class));
|
||||
|
||||
assertThat(type.isEntity()).isTrue();
|
||||
assertThat(type.isAggregateRoot()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void considersEntityAnAggregateRootIfTheresARepositoryForIt() {
|
||||
|
||||
Map<Class<?>, Boolean> parameters = new HashMap<Class<?>, Boolean>();
|
||||
parameters.put(SampleEntity.class, true);
|
||||
parameters.put(OtherEntity.class, false);
|
||||
parameters.put(NoEntity.class, false);
|
||||
|
||||
parameters.entrySet().stream().forEach(it -> {
|
||||
|
||||
JavaClass entity = classes.getRequiredClass(it.getKey());
|
||||
|
||||
assertThat(new SpringDataAwareArchitecturallyEvidentType(entity, classes).isAggregateRoot())
|
||||
.isEqualTo(it.getValue());
|
||||
});
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
Stream<DynamicTest> considersJMoleculesEntity() {
|
||||
|
||||
return DynamicTest.stream(getTypesFor(JMoleculesAnnotatedEntity.class, JMoleculesImplementingEntity.class), //
|
||||
it -> String.format("%s is considered an entity", it.getType().getSimpleName()), //
|
||||
it -> {
|
||||
assertThat(it.isEntity()).isTrue();
|
||||
assertThat(it.isAggregateRoot()).isFalse();
|
||||
assertThat(it.isRepository()).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
Stream<DynamicTest> considersJMoleculesAggregateRoot() {
|
||||
|
||||
return DynamicTest.stream(
|
||||
getTypesFor(JMoleculesAnnotatedAggregateRoot.class, JMoleculesImplementingAggregateRoot.class), //
|
||||
it -> String.format("%s is considered an entity, aggregate root but not a repository",
|
||||
it.getType().getSimpleName()), //
|
||||
it -> {
|
||||
assertThat(it.isEntity()).isTrue();
|
||||
assertThat(it.isAggregateRoot()).isTrue();
|
||||
assertThat(it.isRepository()).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
Stream<DynamicTest> considersJMoleculesRepository() {
|
||||
|
||||
return DynamicTest.stream(getTypesFor(JMoleculesAnnotatedRepository.class), //
|
||||
it -> String.format("%s is considered a repository", it.getType().getSimpleName()), //
|
||||
it -> {
|
||||
assertThat(it.isEntity()).isFalse();
|
||||
assertThat(it.isAggregateRoot()).isFalse();
|
||||
assertThat(it.isRepository()).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void discoversEventsListenedToForEventListener() {
|
||||
|
||||
JavaClass listenerType = classes.getRequiredClass(SomeEventListener.class);
|
||||
|
||||
assertThat(ArchitecturallyEvidentType.of(listenerType, classes).getReferenceTypes()) //
|
||||
.extracting(JavaClass::getFullName) //
|
||||
.containsExactly(Object.class.getName(), String.class.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void discoversImplementingEventListener() {
|
||||
|
||||
JavaClass listenerType = classes.getRequiredClass(ImplementingEventListener.class);
|
||||
|
||||
assertThat(ArchitecturallyEvidentType.of(listenerType, classes).getReferenceTypes()) //
|
||||
.extracting(JavaClass::getFullName) //
|
||||
.containsExactly(ApplicationReadyEvent.class.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void discoversJMoleculesEventHandler() {
|
||||
|
||||
JavaClass type = classes.getRequiredClass(JMoleculesEventListener.class);
|
||||
|
||||
assertThat(ArchitecturallyEvidentType.of(type, classes).isEventListener()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void discoversJMoleculesRepository() {
|
||||
|
||||
JavaClass type = classes.getRequiredClass(JMoleculesImplementingRepository.class);
|
||||
|
||||
assertThat(ArchitecturallyEvidentType.of(type, classes).isRepository()).isTrue();
|
||||
}
|
||||
|
||||
private Iterator<ArchitecturallyEvidentType> getTypesFor(Class<?>... types) {
|
||||
|
||||
return Stream.of(types) //
|
||||
.map(classes::getRequiredClass) //
|
||||
.map(it -> ArchitecturallyEvidentType.of(it, classes)) //
|
||||
.iterator();
|
||||
}
|
||||
|
||||
// Spring
|
||||
|
||||
@Repository
|
||||
interface SpringRepository {}
|
||||
|
||||
@Entity
|
||||
class SampleEntity {}
|
||||
|
||||
// Spring Data
|
||||
|
||||
interface SampleRepository extends CrudRepository<SampleEntity, UUID> {}
|
||||
|
||||
@Entity
|
||||
class OtherEntity {}
|
||||
|
||||
class NoEntity {}
|
||||
|
||||
// jMolecules
|
||||
|
||||
@org.jmolecules.ddd.annotation.Entity
|
||||
class JMoleculesAnnotatedEntity {}
|
||||
|
||||
@org.jmolecules.ddd.annotation.AggregateRoot
|
||||
class JMoleculesAnnotatedAggregateRoot {}
|
||||
|
||||
class JMoleculesImplementingIdentifier implements org.jmolecules.ddd.types.Identifier {}
|
||||
|
||||
abstract class JMoleculesImplementingEntity
|
||||
implements
|
||||
org.jmolecules.ddd.types.Entity<JMoleculesImplementingAggregateRoot, JMoleculesImplementingIdentifier> {}
|
||||
|
||||
abstract class JMoleculesImplementingAggregateRoot
|
||||
implements
|
||||
org.jmolecules.ddd.types.AggregateRoot<JMoleculesImplementingAggregateRoot, JMoleculesImplementingIdentifier> {}
|
||||
|
||||
@org.jmolecules.ddd.annotation.Repository
|
||||
class JMoleculesAnnotatedRepository {}
|
||||
|
||||
interface JMoleculesEventListener {
|
||||
|
||||
@DomainEventHandler
|
||||
void on(Object event);
|
||||
}
|
||||
|
||||
interface JMoleculesImplementingRepository extends
|
||||
org.jmolecules.ddd.types.Repository<JMoleculesImplementingAggregateRoot, JMoleculesImplementingIdentifier> {}
|
||||
|
||||
// Spring
|
||||
|
||||
class SomeEventListener {
|
||||
|
||||
@EventListener
|
||||
void on(Object event) {}
|
||||
|
||||
@EventListener
|
||||
void on(String event) {}
|
||||
|
||||
@EventListener
|
||||
void onOther(Object event) {}
|
||||
}
|
||||
|
||||
class ImplementingEventListener implements ApplicationListener<ApplicationReadyEvent> {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent)
|
||||
*/
|
||||
@Override
|
||||
public void onApplicationEvent(ApplicationReadyEvent event) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.moduliths.model.Module.ModuleDependency;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
import com.tngtech.archunit.core.importer.ClassFileImporter;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ModuleDependency}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
class ModuleDependencyUnitTest {
|
||||
|
||||
ClassFileImporter importer = new ClassFileImporter();
|
||||
|
||||
@Test
|
||||
public void detectsInjectionDependencies() {
|
||||
|
||||
assertThat(findDependencies(SubType.class)) //
|
||||
.containsExactlyInAnyOrder(A.class, B.class, C.class, D.class, E.class, F.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void detectsDependencyFromAnnotatedConstructor() {
|
||||
|
||||
assertThat(findDependencies(MultipleConstructors.class)) //
|
||||
.containsExactlyInAnyOrder(B.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void detectsDependencyFromSingleUnannotatedConstructor() {
|
||||
|
||||
assertThat(findDependencies(SingleConstructor.class)) //
|
||||
.containsExactlyInAnyOrder(B.class);
|
||||
}
|
||||
|
||||
private Stream<Class<?>> findDependencies(Class<?> type) {
|
||||
|
||||
return ModuleDependency.fromType(importer.importClass(type)) //
|
||||
.map(ModuleDependency::getTarget) //
|
||||
.map(JavaClass::reflect);
|
||||
}
|
||||
|
||||
static class A {}
|
||||
|
||||
static class B {}
|
||||
|
||||
static class C {}
|
||||
|
||||
static class D {}
|
||||
|
||||
static class E {}
|
||||
|
||||
static class F {}
|
||||
|
||||
static class SomeComponent {
|
||||
|
||||
@Autowired A a;
|
||||
|
||||
@Autowired
|
||||
void setD(D d) {}
|
||||
}
|
||||
|
||||
static class SubType extends SomeComponent {
|
||||
|
||||
@Autowired E e;
|
||||
|
||||
SubType(B b, C c) {}
|
||||
|
||||
@Autowired
|
||||
void setF(F f) {}
|
||||
}
|
||||
|
||||
static class MultipleConstructors {
|
||||
|
||||
MultipleConstructors(A a) {}
|
||||
|
||||
@Autowired
|
||||
MultipleConstructors(B b) {}
|
||||
}
|
||||
|
||||
static class SingleConstructor {
|
||||
SingleConstructor(B b) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.tngtech.archunit.core.domain.JavaClasses;
|
||||
import com.tngtech.archunit.core.importer.ClassFileImporter;
|
||||
import com.tngtech.archunit.core.importer.ImportOption;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ModuleDetectionStrategy}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
class ModuleDetectionStrategyUnitTest {
|
||||
|
||||
@Test
|
||||
void usesExplicitlyAnnotatedConstant() {
|
||||
|
||||
assertThat(ModuleDetectionStrategy.explictlyAnnotated())
|
||||
.isEqualTo(ModuleDetectionStrategies.EXPLICITLY_ANNOTATED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void usesDirectSubPackages() {
|
||||
|
||||
assertThat(ModuleDetectionStrategy.directSubPackage())
|
||||
.isEqualTo(ModuleDetectionStrategies.DIRECT_SUB_PACKAGES);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectsJMoleculesAnnotatedModule() {
|
||||
|
||||
JavaClasses classes = new ClassFileImporter() //
|
||||
.withImportOption(new ImportOption.OnlyIncludeTests()) //
|
||||
.importPackages("jmolecules");
|
||||
|
||||
JavaPackage javaPackage = JavaPackage.of(Classes.of(classes), "jmolecules");
|
||||
|
||||
assertThat(ModuleDetectionStrategy.explictlyAnnotated().getModuleBasePackages(javaPackage))
|
||||
.containsExactly(javaPackage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2019-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInstance;
|
||||
import org.junit.jupiter.api.TestInstance.Lifecycle;
|
||||
|
||||
import com.acme.withatbean.TestEvents.JMoleculesAnnotated;
|
||||
import com.acme.withatbean.TestEvents.JMoleculesImplementing;
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
import com.tngtech.archunit.core.domain.JavaClasses;
|
||||
import com.tngtech.archunit.core.importer.ClassFileImporter;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link Module}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@TestInstance(Lifecycle.PER_CLASS)
|
||||
class ModuleUnitTest {
|
||||
|
||||
ClassFileImporter importer = new ClassFileImporter();
|
||||
JavaClasses classes = importer.importPackages("com.acme.withatbean"); //
|
||||
JavaPackage javaPackage = JavaPackage.of(Classes.of(classes), "");
|
||||
|
||||
Module module = new Module(javaPackage, false);
|
||||
|
||||
@Test
|
||||
public void considersExternalSpringBeans() {
|
||||
|
||||
assertThat(module.getSpringBeans()) //
|
||||
.flatExtracting(SpringBean::getFullyQualifiedTypeName) //
|
||||
.contains(DataSource.class.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void discoversPublishedEvents() {
|
||||
|
||||
JavaClass jMoleculesAnnotated = classes.get(JMoleculesAnnotated.class);
|
||||
JavaClass jMoleculesImplementing = classes.get(JMoleculesImplementing.class);
|
||||
|
||||
List<EventType> events = module.getPublishedEvents();
|
||||
|
||||
assertThat(events.stream().map(EventType::getType)) //
|
||||
.containsExactlyInAnyOrder(jMoleculesAnnotated, jMoleculesImplementing);
|
||||
assertThat(events.stream().filter(it -> it.getType().equals(jMoleculesAnnotated))) //
|
||||
.element(0) //
|
||||
.satisfies(it -> {
|
||||
assertThat(it.getSources()).isNotEmpty();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.moduliths.Modulith;
|
||||
import org.moduliths.Modulithic;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ModulithMetadata}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
class ModulithMetadataUnitTest {
|
||||
|
||||
@Test
|
||||
public void inspectsModulithAnnotation() throws Exception {
|
||||
|
||||
Stream.of(ModulithAnnotated.class, ModuliticAnnotated.class) //
|
||||
.map(ModulithMetadata::of) //
|
||||
.forEach(it -> {
|
||||
|
||||
assertThat(it.getAdditionalPackages()).containsExactly("com.acme.foo");
|
||||
assertThat(it.getSharedModuleNames()).containsExactly("shared.module");
|
||||
assertThat(it.getSystemName()).hasValue("systemName");
|
||||
assertThat(it.useFullyQualifiedModuleNames()).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void usesDefaultsIfModulithAnnotationsAreMissing() {
|
||||
|
||||
ModulithMetadata metadata = ModulithMetadata.of(SpringBootApplicationAnnotated.class);
|
||||
|
||||
assertThat(metadata.getAdditionalPackages()).isEmpty();
|
||||
assertThat(metadata.getSharedModuleNames()).isEmpty();
|
||||
assertThat(metadata.getSystemName()).isEmpty();
|
||||
assertThat(metadata.useFullyQualifiedModuleNames()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsTypeNotAnnotatedWithEitherModulithAnnotationOrSpringBootApplication() {
|
||||
|
||||
assertThatExceptionOfType(IllegalArgumentException.class) //
|
||||
.isThrownBy(() -> ModulithMetadata.of(Unannotated.class)) //
|
||||
.withMessageContaining(Modulith.class.getSimpleName()) //
|
||||
.withMessageContaining(Modulithic.class.getSimpleName()) //
|
||||
.withMessageContaining(SpringBootApplication.class.getSimpleName());
|
||||
}
|
||||
|
||||
@Modulith(additionalPackages = "com.acme.foo", //
|
||||
sharedModules = "shared.module", //
|
||||
systemName = "systemName", //
|
||||
useFullyQualifiedModuleNames = true)
|
||||
static class ModulithAnnotated {}
|
||||
|
||||
@Modulithic(additionalPackages = "com.acme.foo", //
|
||||
sharedModules = "shared.module", //
|
||||
systemName = "systemName", //
|
||||
useFullyQualifiedModuleNames = true)
|
||||
static class ModuliticAnnotated {}
|
||||
|
||||
@SpringBootApplication
|
||||
static class SpringBootApplicationAnnotated {}
|
||||
|
||||
static class Unannotated {}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
|
||||
|
||||
import org.jmolecules.ddd.annotation.AggregateRoot;
|
||||
import org.springframework.data.repository.Repository;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.tngtech.archunit.base.DescribedPredicate;
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
import com.tngtech.archunit.core.domain.JavaClasses;
|
||||
import com.tngtech.archunit.core.importer.ClassFileImporter;
|
||||
import com.tngtech.archunit.thirdparty.com.google.common.base.Supplier;
|
||||
import com.tngtech.archunit.thirdparty.com.google.common.base.Suppliers;
|
||||
|
||||
/**
|
||||
* Utilities for testing.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
class TestUtils {
|
||||
|
||||
private static Supplier<JavaClasses> imported = Suppliers.memoize(() -> new ClassFileImporter() //
|
||||
.importPackagesOf(Modules.class, Repository.class, AggregateRoot.class));
|
||||
|
||||
private static DescribedPredicate<JavaClass> IS_MODULE_TYPE = JavaClass.Predicates
|
||||
.resideInAPackage(Modules.class.getPackage().getName());
|
||||
|
||||
private static Supplier<Classes> classes = Suppliers.memoize(() -> Classes.of(imported.get()).that(IS_MODULE_TYPE));
|
||||
|
||||
/**
|
||||
* Returns all {@link Classes} of this module.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static Classes getClasses() {
|
||||
return classes.get();
|
||||
}
|
||||
|
||||
public static JavaClasses getJavaClasses() {
|
||||
return imported.get().that(IS_MODULE_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all {@link Classes} in the package of the given type.
|
||||
*
|
||||
* @param packageType must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public static Classes getClasses(Class<?> packageType) {
|
||||
|
||||
Assert.notNull(packageType, "Package type must not be null!");
|
||||
|
||||
return getClasses().that(resideInAPackage(packageType.getPackage().getName()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.model;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link Violations}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
class ViolationsUnitTests {
|
||||
|
||||
@Test
|
||||
void combinesExceptionMessages() {
|
||||
|
||||
Violations violations = Violations.NONE //
|
||||
.and(new IllegalArgumentException("First")) //
|
||||
.and(new IllegalArgumentException("Second"));
|
||||
|
||||
assertThat(violations.getMessage()) //
|
||||
.isEqualTo("- First\n- Second");
|
||||
}
|
||||
}
|
||||
1
moduliths-core/src/test/resources/application.properties
Normal file
1
moduliths-core/src/test/resources/application.properties
Normal file
@@ -0,0 +1 @@
|
||||
spring.main.banner-mode=OFF
|
||||
14
moduliths-core/src/test/resources/logback.xml
Normal file
14
moduliths-core/src/test/resources/logback.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
|
||||
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d %5p %40.40c:%4L - %m%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="OFF">
|
||||
<appender-ref ref="console" />
|
||||
</root>
|
||||
|
||||
</configuration>
|
||||
72
moduliths-docs/pom.xml
Normal file
72
moduliths-docs/pom.xml
Normal file
@@ -0,0 +1,72 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.moduliths</groupId>
|
||||
<artifactId>moduliths</artifactId>
|
||||
<version>1.4.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>Moduliths - Docs</name>
|
||||
<artifactId>moduliths-docs</artifactId>
|
||||
|
||||
<properties>
|
||||
<module.name>org.moduliths.docs</module.name>
|
||||
<structurizr.version>1.3.0</structurizr.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>moduliths-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>moduliths-sample</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.structurizr</groupId>
|
||||
<artifactId>structurizr-core</artifactId>
|
||||
<version>1.9.7</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.structurizr</groupId>
|
||||
<artifactId>structurizr-plantuml</artifactId>
|
||||
<version>1.6.3</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.structurizr</groupId>
|
||||
<artifactId>structurizr-export</artifactId>
|
||||
<version>1.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.jayway.jsonpath</groupId>
|
||||
<artifactId>json-path</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>capital.scalable</groupId>
|
||||
<artifactId>spring-auto-restdocs-core</artifactId>
|
||||
<version>2.0.8</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
358
moduliths-docs/src/main/java/org/moduliths/docs/Asciidoctor.java
Normal file
358
moduliths-docs/src/main/java/org/moduliths/docs/Asciidoctor.java
Normal file
@@ -0,0 +1,358 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.docs;
|
||||
|
||||
import static org.springframework.util.ClassUtils.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.moduliths.docs.ConfigurationProperties.ModuleProperty;
|
||||
import org.moduliths.docs.Documenter.CanvasOptions;
|
||||
import org.moduliths.docs.Documenter.CanvasOptions.Groupings;
|
||||
import org.moduliths.model.ArchitecturallyEvidentType;
|
||||
import org.moduliths.model.EventType;
|
||||
import org.moduliths.model.FormatableJavaClass;
|
||||
import org.moduliths.model.Module;
|
||||
import org.moduliths.model.Modules;
|
||||
import org.moduliths.model.Source;
|
||||
import org.moduliths.model.SpringBean;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
import com.tngtech.archunit.core.domain.JavaMethod;
|
||||
import com.tngtech.archunit.core.domain.JavaModifier;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
class Asciidoctor {
|
||||
|
||||
private static String PLACEHOLDER = "¯\\_(ツ)_/¯";
|
||||
private static final Pattern JAVADOC_CODE = Pattern.compile("\\{\\@(?>link|code|literal)\\s(.*)\\}");
|
||||
|
||||
private final Modules modules;
|
||||
private final String javaDocBase;
|
||||
private final Optional<DocumentationSource> docSource;
|
||||
|
||||
private Asciidoctor(Modules modules, String javaDocBase) {
|
||||
|
||||
Assert.notNull(modules, "Modules must not be null!");
|
||||
Assert.hasText(javaDocBase, "Javadoc base must not be null or empty!");
|
||||
|
||||
this.javaDocBase = javaDocBase;
|
||||
this.modules = modules;
|
||||
this.docSource = Optional.of("capital.scalable.restdocs.javadoc.JavadocReaderImpl")
|
||||
.filter(it -> ClassUtils.isPresent(it, Asciidoctor.class.getClassLoader()))
|
||||
.map(__ -> new SpringAutoRestDocsDocumentationSource())
|
||||
.map(it -> new CodeReplacingDocumentationSource(it, this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link Asciidoctor} instance for the given {@link Modules} and Javadoc base URI.
|
||||
*
|
||||
* @param modules must not be {@literal null}.
|
||||
* @param javadocBase can be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static Asciidoctor withJavadocBase(Modules modules, @Nullable String javadocBase) {
|
||||
return new Asciidoctor(modules, javadocBase == null ? PLACEHOLDER : javadocBase);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link Asciidoctor} instance for the given {@link Modules}.
|
||||
*
|
||||
* @param modules must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static Asciidoctor withoutJavadocBase(Modules modules) {
|
||||
return new Asciidoctor(modules, PLACEHOLDER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns the given source string into inline code.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public String toInlineCode(String source) {
|
||||
|
||||
String[] parts = source.split("#");
|
||||
|
||||
String type = parts[0];
|
||||
Optional<String> methodSignature = parts.length == 2 ? Optional.of(parts[1]) : Optional.empty();
|
||||
|
||||
return modules.getModuleByType(type)
|
||||
.flatMap(it -> it.getType(type))
|
||||
.map(it -> toOptionalLink(it, methodSignature))
|
||||
.orElseGet(() -> String.format("`%s`", type));
|
||||
}
|
||||
|
||||
public String toInlineCode(JavaClass type) {
|
||||
return toOptionalLink(type);
|
||||
}
|
||||
|
||||
public String toInlineCode(SpringBean bean) {
|
||||
|
||||
String base = toInlineCode(bean.toArchitecturallyEvidentType());
|
||||
|
||||
List<JavaClass> interfaces = bean.getInterfacesWithinModule();
|
||||
|
||||
if (interfaces.isEmpty()) {
|
||||
return base;
|
||||
}
|
||||
|
||||
String interfacesAsString = interfaces.stream() //
|
||||
.map(this::toInlineCode) //
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
return String.format("%s implementing %s", base, interfacesAsString);
|
||||
}
|
||||
|
||||
public String renderSpringBeans(CanvasOptions options, Module module) {
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
Groupings groupings = options.groupBeans(module);
|
||||
|
||||
if (groupings.hasOnlyFallbackGroup()) {
|
||||
return toBulletPoints(groupings.byGrouping(CanvasOptions.FALLBACK_GROUP));
|
||||
}
|
||||
|
||||
groupings.forEach((grouping, beans) -> {
|
||||
|
||||
if (beans.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (builder.length() != 0) {
|
||||
builder.append("\n\n");
|
||||
}
|
||||
|
||||
builder.append("_").append(grouping.getName()).append("_");
|
||||
|
||||
if (grouping.getDescription() != null) {
|
||||
builder.append(" -- ").append(grouping.getDescription());
|
||||
}
|
||||
|
||||
builder.append("\n\n");
|
||||
builder.append(toBulletPoints(beans));
|
||||
|
||||
});
|
||||
|
||||
return builder.length() == 0 ? "None" : builder.toString();
|
||||
}
|
||||
|
||||
public String renderEvents(Module module) {
|
||||
|
||||
List<EventType> events = module.getPublishedEvents();
|
||||
|
||||
if (events.isEmpty()) {
|
||||
return "none";
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
for (EventType eventType : events) {
|
||||
|
||||
builder.append("* ")
|
||||
.append(toInlineCode(eventType.getType()));
|
||||
|
||||
if (!eventType.hasSources()) {
|
||||
builder.append("\n");
|
||||
} else {
|
||||
builder.append(" created by:\n");
|
||||
}
|
||||
|
||||
for (Source source : eventType.getSources()) {
|
||||
|
||||
builder.append("** ")
|
||||
.append(toInlineCode(source.toString(module)))
|
||||
.append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public String renderConfigurationProperties(Module module, List<ModuleProperty> properties) {
|
||||
|
||||
if (properties.isEmpty()) {
|
||||
return "none";
|
||||
}
|
||||
|
||||
Stream<String> stream = properties.stream()
|
||||
.map(it -> {
|
||||
|
||||
StringBuilder builder = new StringBuilder()
|
||||
.append(toCode(it.getName()))
|
||||
.append(" -- ")
|
||||
.append(toInlineCode(it.getType()));
|
||||
|
||||
String defaultValue = it.getDefaultValue();
|
||||
|
||||
if (defaultValue != null && StringUtils.hasText(defaultValue)) {
|
||||
|
||||
builder = builder.append(", default ")
|
||||
.append(toInlineCode(defaultValue))
|
||||
.append("");
|
||||
}
|
||||
|
||||
String description = it.getDescription();
|
||||
|
||||
if (description != null && StringUtils.hasText(description)) {
|
||||
builder = builder.append(". ")
|
||||
.append(toAsciidoctor(description));
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
});
|
||||
|
||||
return toBulletPoints(stream);
|
||||
}
|
||||
|
||||
private String toBulletPoints(List<SpringBean> beans) {
|
||||
return toBulletPoints(beans.stream().map(this::toInlineCode));
|
||||
}
|
||||
|
||||
public String typesToBulletPoints(List<JavaClass> types) {
|
||||
return toBulletPoints(types.stream() //
|
||||
.map(this::toOptionalLink));
|
||||
}
|
||||
|
||||
private String toBulletPoints(Stream<String> types) {
|
||||
|
||||
return types//
|
||||
.collect(Collectors.joining("\n* ", "* ", ""));
|
||||
}
|
||||
|
||||
public String toBulletPoint(String source) {
|
||||
return String.format("* %s", source);
|
||||
}
|
||||
|
||||
private String toOptionalLink(JavaClass source) {
|
||||
return toOptionalLink(source, Optional.empty());
|
||||
}
|
||||
|
||||
private String toOptionalLink(JavaClass source, Optional<String> methodSignature) {
|
||||
|
||||
Module module = modules.getModuleByType(source).orElse(null);
|
||||
String typeAndMethod = toCode(
|
||||
toTypeAndMethod(FormatableJavaClass.of(source).getAbbreviatedFullName(module), methodSignature));
|
||||
|
||||
if (module == null
|
||||
|| !source.getModifiers().contains(JavaModifier.PUBLIC)
|
||||
|| !module.contains(source)) {
|
||||
return typeAndMethod;
|
||||
}
|
||||
|
||||
String classPath = convertClassNameToResourcePath(source.getFullName()) //
|
||||
.replace('$', '.');
|
||||
|
||||
return Optional.ofNullable(javaDocBase == PLACEHOLDER ? null : javaDocBase) //
|
||||
.map(it -> it.concat("/").concat(classPath).concat(".html")) //
|
||||
.map(it -> toLink(typeAndMethod, it)) //
|
||||
.orElseGet(() -> typeAndMethod);
|
||||
}
|
||||
|
||||
private static String toTypeAndMethod(String type, Optional<String> methodSignature) {
|
||||
return methodSignature
|
||||
.map(it -> type.concat("#").concat(it))
|
||||
.orElse(type);
|
||||
}
|
||||
|
||||
private String toInlineCode(ArchitecturallyEvidentType type) {
|
||||
|
||||
if (type.isEventListener()) {
|
||||
|
||||
if (!docSource.isPresent()) {
|
||||
|
||||
Stream<JavaClass> referenceTypes = type.getReferenceTypes();
|
||||
|
||||
return String.format("%s listening to %s", //
|
||||
toInlineCode(type.getType()), //
|
||||
toInlineCode(referenceTypes));
|
||||
}
|
||||
|
||||
String header = String.format("%s listening to:\n", toInlineCode(type.getType()));
|
||||
|
||||
return header + type.getReferenceMethods().map(it -> {
|
||||
|
||||
JavaMethod method = it.getMethod();
|
||||
Assert.isTrue(method.getRawParameterTypes().size() > 0,
|
||||
() -> String.format("Method %s must have at least one parameter!", method));
|
||||
|
||||
JavaClass parameterType = it.getMethod().getRawParameterTypes().get(0);
|
||||
String isAsync = it.isAsync() ? "(async) " : "";
|
||||
|
||||
return docSource.flatMap(source -> source.getDocumentation(it.getMethod()))
|
||||
.map(doc -> String.format("** %s %s-- %s", toInlineCode(parameterType), isAsync, doc))
|
||||
.orElseGet(() -> String.format("** %s %s", toInlineCode(parameterType), isAsync));
|
||||
|
||||
}).collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
return toInlineCode(type.getType());
|
||||
}
|
||||
|
||||
private String toInlineCode(Stream<JavaClass> types) {
|
||||
|
||||
return types.map(this::toInlineCode) //
|
||||
.collect(Collectors.joining(", "));
|
||||
}
|
||||
|
||||
private static String toLink(String source, String href) {
|
||||
return String.format("link:%s[%s]", href, source);
|
||||
}
|
||||
|
||||
private static String toCode(String source) {
|
||||
return String.format("`%s`", source);
|
||||
}
|
||||
|
||||
public static String startTable(String tableSpec) {
|
||||
return String.format("[%s]\n|===\n", tableSpec);
|
||||
}
|
||||
|
||||
public static String startOrEndTable() {
|
||||
return "|===\n";
|
||||
}
|
||||
|
||||
public static String writeTableRow(String... columns) {
|
||||
|
||||
return Stream.of(columns) //
|
||||
.collect(Collectors.joining("\n|", "|", "\n"));
|
||||
}
|
||||
|
||||
public String toAsciidoctor(String source) {
|
||||
|
||||
Matcher matcher = JAVADOC_CODE.matcher(source);
|
||||
|
||||
while (matcher.find()) {
|
||||
|
||||
String type = matcher.group(1);
|
||||
|
||||
source = source.replace(matcher.group(), toInlineCode(type));
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.docs;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import com.tngtech.archunit.core.domain.JavaMethod;
|
||||
|
||||
/**
|
||||
* A {@link DocumentationSource} that replaces {@literal {@code …}} or {@literal {@link …}} blocks into inline code
|
||||
* references
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 1.1
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
class CodeReplacingDocumentationSource implements DocumentationSource {
|
||||
|
||||
private final DocumentationSource delegate;
|
||||
private final Asciidoctor codeSource;
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.docs.DocumentationSource#getDocumentation(com.tngtech.archunit.core.domain.JavaMethod)
|
||||
*/
|
||||
@Override
|
||||
public Optional<String> getDocumentation(JavaMethod method) {
|
||||
|
||||
return delegate.getDocumentation(method)
|
||||
.map(codeSource::toAsciidoctor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.docs;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.moduliths.docs.ConfigurationProperties.ConfigurationProperty;
|
||||
import org.moduliths.model.Module;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.jayway.jsonpath.DocumentContext;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import com.tngtech.archunit.core.domain.JavaType;
|
||||
|
||||
/**
|
||||
* Represents all {@link ConfigurationProperty} instances found for the current project.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 1.3
|
||||
*/
|
||||
class ConfigurationProperties implements Iterable<ConfigurationProperty> {
|
||||
|
||||
private static final String METADATA_PATH = "classpath:META-INF/spring-configuration-metadata.json";
|
||||
private static final JsonPath PATH = JsonPath.compile("$.properties");
|
||||
|
||||
private final List<ConfigurationProperty> properties;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ConfigurationProperties} instance.
|
||||
*/
|
||||
ConfigurationProperties() {
|
||||
|
||||
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
|
||||
|
||||
try {
|
||||
Resource[] resources = resolver.getResources(METADATA_PATH);
|
||||
|
||||
this.properties = Arrays.stream(resources)
|
||||
.flatMap(ConfigurationProperties::parseProperties)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all {@link ModuleProperty} instances for the given {@link Module}.
|
||||
*
|
||||
* @param module must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public List<ModuleProperty> getModuleProperties(Module module) {
|
||||
|
||||
Assert.notNull(module, "Module must not be null!");
|
||||
|
||||
return properties.stream()
|
||||
.flatMap(it -> getModuleProperty(module, it))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Iterable#iterator()
|
||||
*/
|
||||
@Override
|
||||
public Iterator<ConfigurationProperty> iterator() {
|
||||
return properties.iterator();
|
||||
}
|
||||
|
||||
private Stream<ModuleProperty> getModuleProperty(Module module,
|
||||
ConfigurationProperty property) {
|
||||
|
||||
return module.getType(property.getSourceType())
|
||||
.map(it -> new ModuleProperty(property.getName(), property.getDescription(), property.getType(), it,
|
||||
property.getDefaultValue()))
|
||||
.map(Stream::of)
|
||||
.orElseGet(Stream::empty);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Stream<ConfigurationProperty> parseProperties(Resource source) {
|
||||
|
||||
if (!source.exists()) {
|
||||
return Stream.empty();
|
||||
}
|
||||
|
||||
try (InputStream stream = source.getInputStream()) {
|
||||
|
||||
DocumentContext context = JsonPath.parse(stream);
|
||||
List<Object> read = context.read(PATH, List.class);
|
||||
|
||||
return read.stream()
|
||||
.map(it -> (Map<String, Object>) it)
|
||||
.flatMap(ConfigurationProperty::of);
|
||||
|
||||
} catch (Exception o_O) {
|
||||
return Stream.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Value
|
||||
static class ConfigurationProperty {
|
||||
|
||||
String name;
|
||||
@Nullable String description;
|
||||
String type, sourceType;
|
||||
@Nullable String defaultValue;
|
||||
|
||||
@SuppressWarnings("null")
|
||||
static Stream<ConfigurationProperty> of(Map<String, Object> source) {
|
||||
|
||||
String sourceType = getAsString(source, "sourceType");
|
||||
|
||||
if (!StringUtils.hasText(sourceType)) {
|
||||
return Stream.empty();
|
||||
}
|
||||
|
||||
ConfigurationProperty property = new ConfigurationProperty(getAsString(source, "name"),
|
||||
getAsString(source, "description"),
|
||||
getAsString(source, "type"),
|
||||
sourceType,
|
||||
getAsString(source, "defaultValue"));
|
||||
|
||||
return Stream.of(property);
|
||||
}
|
||||
|
||||
boolean hasSourceType() {
|
||||
return StringUtils.hasText(sourceType);
|
||||
}
|
||||
|
||||
private static @Nullable String getAsString(Map<String, Object> source, String key) {
|
||||
|
||||
Object value = source.get(key);
|
||||
|
||||
return value == null ? null : value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@Value
|
||||
static class ModuleProperty {
|
||||
String name;
|
||||
@Nullable String description;
|
||||
String type;
|
||||
JavaType sourceType;
|
||||
@Nullable String defaultValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.docs;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import com.tngtech.archunit.core.domain.JavaMethod;
|
||||
|
||||
/**
|
||||
* Interface to abstract different ways of looking up documentation for code abstractions.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 1.1
|
||||
*/
|
||||
interface DocumentationSource {
|
||||
|
||||
/**
|
||||
* Returns the documentation to be used for the given {@link JavaMethod}.
|
||||
*
|
||||
* @param method must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
Optional<String> getDocumentation(JavaMethod method);
|
||||
}
|
||||
821
moduliths-docs/src/main/java/org/moduliths/docs/Documenter.java
Normal file
821
moduliths-docs/src/main/java/org/moduliths/docs/Documenter.java
Normal file
@@ -0,0 +1,821 @@
|
||||
/*
|
||||
* Copyright 2018-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.docs;
|
||||
|
||||
import static org.moduliths.docs.Asciidoctor.*;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Value;
|
||||
import lombok.With;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.io.Writer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.moduliths.model.Module;
|
||||
import org.moduliths.model.Module.DependencyDepth;
|
||||
import org.moduliths.model.Module.DependencyType;
|
||||
import org.moduliths.model.Modules;
|
||||
import org.moduliths.model.SpringBean;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import com.structurizr.Workspace;
|
||||
import com.structurizr.io.Diagram;
|
||||
import com.structurizr.io.plantuml.BasicPlantUMLWriter;
|
||||
import com.structurizr.io.plantuml.C4PlantUMLExporter;
|
||||
import com.structurizr.io.plantuml.PlantUMLWriter;
|
||||
import com.structurizr.model.Component;
|
||||
import com.structurizr.model.Container;
|
||||
import com.structurizr.model.Element;
|
||||
import com.structurizr.model.Model;
|
||||
import com.structurizr.model.Relationship;
|
||||
import com.structurizr.model.SoftwareSystem;
|
||||
import com.structurizr.model.Tags;
|
||||
import com.structurizr.view.ComponentView;
|
||||
import com.structurizr.view.RelationshipView;
|
||||
import com.structurizr.view.Shape;
|
||||
import com.structurizr.view.Styles;
|
||||
import com.structurizr.view.View;
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
|
||||
/**
|
||||
* API to create documentation for {@link Modules}.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class Documenter {
|
||||
|
||||
private static final Map<DependencyType, String> DEPENDENCY_DESCRIPTIONS = new LinkedHashMap<>();
|
||||
|
||||
private static final String INVALID_FILE_NAME_PATTERN = "Configured file name pattern does not include a '%s' placeholder for the module name!";
|
||||
|
||||
static {
|
||||
DEPENDENCY_DESCRIPTIONS.put(DependencyType.EVENT_LISTENER, "listens to");
|
||||
DEPENDENCY_DESCRIPTIONS.put(DependencyType.DEFAULT, "depends on");
|
||||
}
|
||||
|
||||
private final @Getter Modules modules;
|
||||
private final Workspace workspace;
|
||||
private final Container container;
|
||||
private final ConfigurationProperties properties;
|
||||
private final String outputFolder;
|
||||
|
||||
private Map<Module, Component> components;
|
||||
|
||||
/**
|
||||
* Creates a new {@link Documenter} for the {@link Modules} created for the given modulith type.
|
||||
*
|
||||
* @param modulithType must not be {@literal null}.
|
||||
*/
|
||||
public Documenter(Class<?> modulithType) {
|
||||
this(Modules.of(modulithType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link Documenter} for the given {@link Modules} instance.
|
||||
*
|
||||
* @param modules must not be {@literal null}.
|
||||
*/
|
||||
public Documenter(Modules modules) {
|
||||
this(modules, getDefaultOutputDirectory());
|
||||
}
|
||||
|
||||
private Documenter(Modules modules, String outputFolder) {
|
||||
|
||||
Assert.notNull(modules, "Modules must not be null!");
|
||||
Assert.hasText(outputFolder, "Output folder must not be null or empty!");
|
||||
|
||||
this.modules = modules;
|
||||
this.outputFolder = outputFolder;
|
||||
this.workspace = new Workspace("Modulith", "");
|
||||
|
||||
workspace.getViews().getConfiguration()
|
||||
.getStyles()
|
||||
.addElementStyle(Tags.COMPONENT)
|
||||
.shape(Shape.Component);
|
||||
|
||||
Model model = workspace.getModel();
|
||||
String systemName = modules.getSystemName().orElse("Modulith");
|
||||
|
||||
SoftwareSystem system = model.addSoftwareSystem(systemName, "");
|
||||
|
||||
this.container = system.addContainer("Application", "", "");
|
||||
this.properties = new ConfigurationProperties();
|
||||
}
|
||||
|
||||
private Map<Module, Component> getComponents(Options options) {
|
||||
|
||||
if (components == null) {
|
||||
|
||||
this.components = modules.stream() //
|
||||
.collect(Collectors.toMap(Function.identity(),
|
||||
it -> container.addComponent(options.getDefaultDisplayName().apply(it), "", "Module")));
|
||||
|
||||
this.components.forEach((key, value) -> addDependencies(key, value, options));
|
||||
}
|
||||
|
||||
return components;
|
||||
}
|
||||
|
||||
/**
|
||||
* Customize the output folder to write the generated files to. Defaults to {@value #DEFAULT_LOCATION}.
|
||||
*
|
||||
* @param outputFolder must not be {@literal null} or empty.
|
||||
* @return
|
||||
* @see #DEFAULT_LOCATION
|
||||
*/
|
||||
public Documenter withOutputFolder(String outputFolder) {
|
||||
return new Documenter(modules, workspace, container, properties, outputFolder, components);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes all available documentation:
|
||||
* <ul>
|
||||
* <li>The entire set of modules as overview component diagram.</li>
|
||||
* <li>Individual component diagrams per module to include all upstream modules.</li>
|
||||
* <li>The Module Canvas for each module.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param options must not be {@literal null}, use {@link Options#defaults()} for default.
|
||||
* @param canvasOptions must not be {@literal null}, use {@link CanvasOptions#defaults()} for default.
|
||||
* @return the current instance, will never be {@literal null}.
|
||||
* @throws IOException
|
||||
* @since 1.1
|
||||
*/
|
||||
public Documenter writeDocumentation(Options options, CanvasOptions canvasOptions) throws IOException {
|
||||
|
||||
return writeModulesAsPlantUml(options)
|
||||
.writeIndividualModulesAsPlantUml(options) //
|
||||
.writeModuleCanvases(canvasOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the PlantUML component diagram for all {@link Modules}.
|
||||
*
|
||||
* @param options must not be {@literal null}.
|
||||
* @throws IOException
|
||||
*/
|
||||
public Documenter writeModulesAsPlantUml(Options options) throws IOException {
|
||||
|
||||
Assert.notNull(options, "Options must not be null!");
|
||||
|
||||
Path file = recreateFile(options.getTargetFileName().orElse("components.uml"));
|
||||
|
||||
try (Writer writer = new FileWriter(file.toFile())) {
|
||||
writer.write(createPlantUml(options));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the component diagrams for all individual modules.
|
||||
*
|
||||
* @param options must not be {@literal null}.
|
||||
* @return the current instance, will never be {@literal null}.
|
||||
* @since 1.1
|
||||
*/
|
||||
public Documenter writeIndividualModulesAsPlantUml(Options options) {
|
||||
|
||||
modules.forEach(it -> writeModuleAsPlantUml(it, options));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the PlantUML component diagram for the given {@link Module}.
|
||||
*
|
||||
* @param module must not be {@literal null}.
|
||||
* @return the current instance, will never be {@literal null}.
|
||||
*/
|
||||
public Documenter writeModuleAsPlantUml(Module module) {
|
||||
|
||||
Assert.notNull(module, "Module must not be null!");
|
||||
|
||||
return writeModuleAsPlantUml(module, Options.defaults());
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the PlantUML component diagram for the given {@link Module} with the given rendering {@link Options}.
|
||||
*
|
||||
* @param module must not be {@literal null}.
|
||||
* @param options must not be {@literal null}.
|
||||
* @return the current instance, will never be {@literal null}.
|
||||
*/
|
||||
public Documenter writeModuleAsPlantUml(Module module, Options options) {
|
||||
|
||||
Assert.notNull(module, "Module must not be null!");
|
||||
Assert.notNull(options, "Options must not be null!");
|
||||
|
||||
ComponentView view = createComponentView(options, module);
|
||||
view.setTitle(options.getDefaultDisplayName().apply(module));
|
||||
|
||||
addComponentsToView(module, view, options);
|
||||
|
||||
String fileNamePattern = options.getTargetFileName().orElse("module-%s.uml");
|
||||
|
||||
Assert.isTrue(fileNamePattern.contains("%s"), () -> String.format(INVALID_FILE_NAME_PATTERN, fileNamePattern));
|
||||
|
||||
return writeViewAsPlantUml(view, String.format(fileNamePattern, module.getName()), options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes all module canvases using {@link Options#defaults()}.
|
||||
*
|
||||
* @return the current instance, will never be {@literal null}.
|
||||
*/
|
||||
public Documenter writeModuleCanvases() {
|
||||
return writeModuleCanvases(CanvasOptions.defaults());
|
||||
}
|
||||
|
||||
public Documenter writeModuleCanvases(CanvasOptions options) {
|
||||
|
||||
modules.forEach(module -> {
|
||||
|
||||
String filename = String.format(options.getTargetFileName().orElse("module-%s.adoc"), module.getName());
|
||||
Path file = recreateFile(filename);
|
||||
|
||||
try (FileWriter writer = new FileWriter(file.toFile())) {
|
||||
|
||||
writer.write(toModuleCanvas(module, options));
|
||||
|
||||
} catch (IOException o_O) {
|
||||
throw new RuntimeException(o_O);
|
||||
}
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param javadocBase
|
||||
* @deprecated since 1.1, use {@link #writeModuleCanvases(CanvasOptions)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public Documenter writeModuleCanvases(String javadocBase) {
|
||||
return writeModuleCanvases(CanvasOptions.defaults().withApiBase(javadocBase));
|
||||
}
|
||||
|
||||
public String toModuleCanvas(Module module) {
|
||||
return toModuleCanvas(module, CanvasOptions.defaults());
|
||||
}
|
||||
|
||||
public String toModuleCanvas(Module module, String apiBase) {
|
||||
return toModuleCanvas(module, CanvasOptions.defaults().withApiBase(apiBase));
|
||||
}
|
||||
|
||||
public String toModuleCanvas(Module module, CanvasOptions options) {
|
||||
|
||||
Asciidoctor asciidoctor = Asciidoctor.withJavadocBase(modules, options.getApiBase());
|
||||
Function<List<JavaClass>, String> mapper = asciidoctor::typesToBulletPoints;
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(startTable("%autowidth.stretch, cols=\"h,a\""));
|
||||
builder.append(writeTableRow("Base package", asciidoctor.toInlineCode(module.getBasePackage().getName())));
|
||||
builder.append(writeTableRow("Spring components", asciidoctor.renderSpringBeans(options, module)));
|
||||
builder.append(addTableRow(module.getAggregateRoots(), "Aggregate roots", mapper));
|
||||
builder.append(writeTableRow("Published events", asciidoctor.renderEvents(module)));
|
||||
builder.append(addTableRow(module.getEventsListenedTo(modules), "Events listened to", mapper));
|
||||
builder.append(writeTableRow("Properties",
|
||||
asciidoctor.renderConfigurationProperties(module, properties.getModuleProperties(module))));
|
||||
builder.append(startOrEndTable());
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private <T> String addTableRow(List<T> types, String header, Function<List<T>, String> mapper) {
|
||||
return types.isEmpty() ? "" : writeTableRow(header, mapper.apply(types));
|
||||
}
|
||||
|
||||
public String toPlantUml() throws IOException {
|
||||
return createPlantUml(Options.defaults());
|
||||
}
|
||||
|
||||
private void addDependencies(Module module, Component component, Options options) {
|
||||
|
||||
DEPENDENCY_DESCRIPTIONS.entrySet().stream().forEach(entry -> {
|
||||
|
||||
module.getDependencies(modules, entry.getKey()).stream() //
|
||||
.map(it -> getComponents(options).get(it)) //
|
||||
// .filter(it -> !component.hasEfferentRelationshipWith(it)) //
|
||||
.forEach(it -> {
|
||||
|
||||
Relationship relationship = component.uses(it, entry.getValue());
|
||||
relationship.addTags(entry.getKey().toString());
|
||||
});
|
||||
});
|
||||
|
||||
module.getBootstrapDependencies(modules) //
|
||||
.forEach(it -> {
|
||||
Relationship relationship = component.uses(getComponents(options).get(it), "uses");
|
||||
relationship.addTags(DependencyType.USES_COMPONENT.toString());
|
||||
});
|
||||
}
|
||||
|
||||
private void addComponentsToView(Module module, ComponentView view, Options options) {
|
||||
|
||||
Supplier<Stream<Module>> bootstrapDependencies = () -> module.getBootstrapDependencies(modules,
|
||||
options.getDependencyDepth());
|
||||
Supplier<Stream<Module>> otherDependencies = () -> options.getDependencyTypes()
|
||||
.flatMap(it -> module.getDependencies(modules, it).stream());
|
||||
|
||||
Supplier<Stream<Module>> dependencies = () -> Stream.concat(bootstrapDependencies.get(), otherDependencies.get());
|
||||
|
||||
addComponentsToView(dependencies, view, options, it -> it.add(getComponents(options).get(module)));
|
||||
}
|
||||
|
||||
private void addComponentsToView(Supplier<Stream<Module>> modules, ComponentView view, Options options,
|
||||
Consumer<ComponentView> afterCleanup) {
|
||||
|
||||
Styles styles = view.getViewSet().getConfiguration().getStyles();
|
||||
Map<Module, Component> components = getComponents(options);
|
||||
|
||||
modules.get() //
|
||||
.distinct()
|
||||
.filter(options.getExclusions().negate()) //
|
||||
.map(it -> applyBackgroundColor(it, components, options, styles)) //
|
||||
.filter(options.getComponentFilter()) //
|
||||
.forEach(view::add);
|
||||
|
||||
// view.getViewSet().getConfiguration().getStyles().findElementStyle(element).getBackground()
|
||||
|
||||
// Remove filtered dependency types
|
||||
DependencyType.allBut(options.getDependencyTypes()) //
|
||||
.map(Object::toString) //
|
||||
.forEach(it -> view.removeRelationshipsWithTag(it));
|
||||
|
||||
afterCleanup.accept(view);
|
||||
|
||||
// Filter outgoing relationships of target-only modules
|
||||
modules.get().filter(options.getTargetOnly()) //
|
||||
.forEach(module -> {
|
||||
|
||||
Component component = components.get(module);
|
||||
|
||||
view.getRelationships().stream() //
|
||||
.map(RelationshipView::getRelationship) //
|
||||
.filter(it -> it.getSource().equals(component)) //
|
||||
.forEach(it -> view.remove(it));
|
||||
});
|
||||
|
||||
// … as well as all elements left without a relationship
|
||||
if (options.hideElementsWithoutRelationships()) {
|
||||
view.removeElementsWithNoRelationships();
|
||||
}
|
||||
|
||||
afterCleanup.accept(view);
|
||||
|
||||
// Remove default relationships if more qualified ones exist
|
||||
view.getRelationships().stream() //
|
||||
.map(RelationshipView::getRelationship) //
|
||||
.collect(Collectors.groupingBy(Connection::of)) //
|
||||
.values().stream() //
|
||||
.forEach(it -> potentiallyRemoveDefaultRelationship(view, it));
|
||||
}
|
||||
|
||||
private void potentiallyRemoveDefaultRelationship(View view, Collection<Relationship> relationships) {
|
||||
|
||||
if (relationships.size() <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
relationships.stream().filter(it -> it.getTagsAsSet().contains(DependencyType.DEFAULT.toString())) //
|
||||
.findFirst().ifPresent(view::remove);
|
||||
}
|
||||
|
||||
private static Component applyBackgroundColor(Module module, Map<Module, Component> components, Options options,
|
||||
Styles styles) {
|
||||
|
||||
Component component = components.get(module);
|
||||
Function<Module, Optional<String>> selector = options.getColorSelector();
|
||||
|
||||
// Apply custom color if configured
|
||||
selector.apply(module).ifPresent(color -> {
|
||||
|
||||
String tag = module.getName() + "-" + color;
|
||||
component.addTags(tag);
|
||||
|
||||
// Add or update background color
|
||||
styles.getElements().stream()
|
||||
.filter(it -> it.getTag().equals(tag))
|
||||
.findFirst()
|
||||
.orElseGet(() -> styles.addElementStyle(tag))
|
||||
.background(color);
|
||||
});
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
private Documenter writeViewAsPlantUml(ComponentView view, String filename, Options options) {
|
||||
|
||||
Path file = recreateFile(filename);
|
||||
|
||||
try (Writer writer = new FileWriter(file.toFile())) {
|
||||
|
||||
writer.write(render(view, options));
|
||||
|
||||
return this;
|
||||
|
||||
} catch (IOException o_O) {
|
||||
throw new RuntimeException(o_O);
|
||||
}
|
||||
}
|
||||
|
||||
private String render(ComponentView view, Options options) {
|
||||
|
||||
switch (options.style) {
|
||||
|
||||
case C4:
|
||||
|
||||
C4PlantUMLExporter exporter = new C4PlantUMLExporter();
|
||||
Diagram diagram = exporter.export(view);
|
||||
return diagram.getDefinition();
|
||||
|
||||
case UML:
|
||||
default:
|
||||
|
||||
Writer writer = new StringWriter();
|
||||
PlantUMLWriter umlWriter = new BasicPlantUMLWriter();
|
||||
umlWriter.addSkinParam("componentStyle", "uml1");
|
||||
umlWriter.write(view, writer);
|
||||
|
||||
return writer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private String createPlantUml(Options options) throws IOException {
|
||||
|
||||
ComponentView componentView = createComponentView(options);
|
||||
componentView.setTitle(modules.getSystemName().orElse("Modules"));
|
||||
|
||||
addComponentsToView(() -> modules.stream(), componentView, options, it -> {});
|
||||
|
||||
return render(componentView, options);
|
||||
}
|
||||
|
||||
private ComponentView createComponentView(Options options) {
|
||||
return createComponentView(options, null);
|
||||
}
|
||||
|
||||
private ComponentView createComponentView(Options options, @Nullable Module module) {
|
||||
|
||||
String prefix = module == null ? "modules-" : module.getName();
|
||||
|
||||
return workspace.getViews() //
|
||||
.createComponentView(container, prefix + options.toString(), "");
|
||||
}
|
||||
|
||||
private Path recreateFile(String name) {
|
||||
|
||||
try {
|
||||
|
||||
Files.createDirectories(Paths.get(outputFolder));
|
||||
Path filePath = Paths.get(outputFolder, name);
|
||||
Files.deleteIfExists(filePath);
|
||||
|
||||
return Files.createFile(filePath);
|
||||
|
||||
} catch (IOException o_O) {
|
||||
throw new RuntimeException(o_O);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default output directory based on the detected build system.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
private static String getDefaultOutputDirectory() {
|
||||
return (new File("pom.xml").exists() ? "target" : "build").concat("/moduliths-docs");
|
||||
}
|
||||
|
||||
@Value
|
||||
private static class Connection {
|
||||
|
||||
Element source, target;
|
||||
|
||||
public static Connection of(Relationship relationship) {
|
||||
return new Connection(relationship.getSource(), relationship.getDestination());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options to tweak the rendering of diagrams.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@Getter(AccessLevel.PRIVATE)
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class Options {
|
||||
|
||||
private static Set<DependencyType> ALL_TYPES = Arrays.stream(DependencyType.values()).collect(Collectors.toSet());
|
||||
|
||||
private final Set<DependencyType> dependencyTypes;
|
||||
|
||||
/**
|
||||
* The {@link DependencyDepth} to define which other modules to be included in the diagram to be created.
|
||||
*/
|
||||
private final @With DependencyDepth dependencyDepth;
|
||||
|
||||
/**
|
||||
* A {@link Predicate} to define the which modules to exclude from the diagram to be created.
|
||||
*/
|
||||
private final @With Predicate<Module> exclusions;
|
||||
|
||||
/**
|
||||
* A {@link Predicate} to define which Structurizr {@link Component}s to be included in the diagram to be created.
|
||||
*/
|
||||
private final @With Predicate<Component> componentFilter;
|
||||
|
||||
/**
|
||||
* A {@link Predicate} to define which of the modules shall only be considered targets, i.e. all efferent
|
||||
* relationships are going to be hidden from the rendered view. Modules that have no incoming relationships will
|
||||
* entirely be removed from the view.
|
||||
*/
|
||||
private final @With Predicate<Module> targetOnly;
|
||||
|
||||
/**
|
||||
* The target file name to be used for the diagram to be created. For individual module diagrams this needs to
|
||||
* include a {@code %s} placeholder for the module names.
|
||||
*/
|
||||
private final @With @Nullable String targetFileName;
|
||||
|
||||
/**
|
||||
* A callback to return a hex-encoded color per {@link Module}.
|
||||
*/
|
||||
private final @With Function<Module, Optional<String>> colorSelector;
|
||||
|
||||
/**
|
||||
* A callback to return a default display names for a given {@link Module}. Default implementation just forwards to
|
||||
* {@link Module#getDisplayName()}.
|
||||
*/
|
||||
private final @With Function<Module, String> defaultDisplayName;
|
||||
|
||||
/**
|
||||
* Which style to render the diagram in. Defaults to {@value DiagramStyle#UML}.
|
||||
*/
|
||||
private final @With DiagramStyle style;
|
||||
|
||||
/**
|
||||
* Configuration setting to define whether modules that do not have a relationship to any other module shall be
|
||||
* retained in the diagrams created. The default is {@value ElementsWithoutRelationships#HIDDEN}. See
|
||||
* {@link Options#withExclusions(Predicate)} for a more fine-grained way of defining which modules to exclude in
|
||||
* case you flip this to {@link ElementsWithoutRelationships#VISIBLE}.
|
||||
*
|
||||
* @see #withExclusions(Predicate)
|
||||
*/
|
||||
private final @With ElementsWithoutRelationships elementsWithoutRelationships;
|
||||
|
||||
/**
|
||||
* Creates a new default {@link Options} instance configured to use all dependency types, list immediate
|
||||
* dependencies for individual module instances, not applying any kind of {@link Module} or {@link Component}
|
||||
* filters and default file names.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static Options defaults() {
|
||||
return new Options(ALL_TYPES, DependencyDepth.IMMEDIATE, it -> false, it -> true, it -> false, null,
|
||||
__ -> Optional.empty(), it -> it.getDisplayName(), DiagramStyle.UML, ElementsWithoutRelationships.HIDDEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the dependency types that are supposed to be included in the diagram to be created.
|
||||
*
|
||||
* @param types must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public Options withDependencyTypes(DependencyType... types) {
|
||||
|
||||
Assert.notNull(types, "Dependency types must not be null!");
|
||||
|
||||
Set<DependencyType> dependencyTypes = Arrays.stream(types).collect(Collectors.toSet());
|
||||
|
||||
return new Options(dependencyTypes, dependencyDepth, exclusions, componentFilter, targetOnly, targetFileName,
|
||||
colorSelector, defaultDisplayName, style, elementsWithoutRelationships);
|
||||
}
|
||||
|
||||
private Optional<String> getTargetFileName() {
|
||||
return Optional.ofNullable(targetFileName);
|
||||
}
|
||||
|
||||
private Stream<DependencyType> getDependencyTypes() {
|
||||
return dependencyTypes.stream();
|
||||
}
|
||||
|
||||
private boolean hideElementsWithoutRelationships() {
|
||||
return elementsWithoutRelationships.equals(ElementsWithoutRelationships.HIDDEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Different diagram styles.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public enum DiagramStyle {
|
||||
|
||||
/**
|
||||
* A plain UML component diagram.
|
||||
*/
|
||||
UML,
|
||||
|
||||
/**
|
||||
* A C4 model component diagram.
|
||||
*
|
||||
* @see https://c4model.com/#ComponentDiagram
|
||||
*/
|
||||
C4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration setting to define whether modules that do not have a relationship to any other module shall be
|
||||
* retained in the diagrams created. The default is {@value ElementsWithoutRelationships#HIDDEN}. See
|
||||
* {@link Options#withExclusions(Predicate)} for a more fine-grained way of defining which modules to exclude in
|
||||
* case you flip this to {@link ElementsWithoutRelationships#VISIBLE}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @see Options#withExclusions(Predicate)
|
||||
*/
|
||||
public enum ElementsWithoutRelationships {
|
||||
HIDDEN, VISIBLE;
|
||||
}
|
||||
}
|
||||
|
||||
// Prefix required for javac 🤔
|
||||
@lombok.RequiredArgsConstructor(access = AccessLevel.PACKAGE)
|
||||
public static class CanvasOptions {
|
||||
|
||||
static final Grouping FALLBACK_GROUP = Grouping.of("Others", null, __ -> true);
|
||||
|
||||
private final List<Grouping> groupers;
|
||||
private final @With @Getter @Nullable String apiBase;
|
||||
private final @With @Nullable String targetFileName;
|
||||
|
||||
public static CanvasOptions defaults() {
|
||||
|
||||
return withoutDefaultGroupings()
|
||||
.groupingBy("Controllers", bean -> bean.toArchitecturallyEvidentType().isController()) //
|
||||
.groupingBy("Services", bean -> bean.toArchitecturallyEvidentType().isService()) //
|
||||
.groupingBy("Repositories", bean -> bean.toArchitecturallyEvidentType().isRepository()) //
|
||||
.groupingBy("Event listeners", bean -> bean.toArchitecturallyEvidentType().isEventListener()) //
|
||||
.groupingBy("Configuration properties",
|
||||
bean -> bean.toArchitecturallyEvidentType().isConfigurationProperties());
|
||||
}
|
||||
|
||||
public static CanvasOptions withoutDefaultGroupings() {
|
||||
return new CanvasOptions(new ArrayList<>(), null, null);
|
||||
}
|
||||
|
||||
public CanvasOptions groupingBy(Grouping... groupings) {
|
||||
|
||||
List<Grouping> result = new ArrayList<>(groupers);
|
||||
result.addAll(Arrays.asList(groupings));
|
||||
|
||||
return new CanvasOptions(result, apiBase, targetFileName);
|
||||
}
|
||||
|
||||
public CanvasOptions groupingBy(String name, Predicate<SpringBean> filter) {
|
||||
return groupingBy(Grouping.of(name, null, filter));
|
||||
}
|
||||
|
||||
Groupings groupBeans(Module module) {
|
||||
|
||||
List<Grouping> sources = new ArrayList<>(groupers);
|
||||
sources.add(FALLBACK_GROUP);
|
||||
|
||||
MultiValueMap<Grouping, SpringBean> result = new LinkedMultiValueMap<>();
|
||||
List<SpringBean> alreadyMapped = new ArrayList<>();
|
||||
|
||||
sources.forEach(it -> {
|
||||
|
||||
List<SpringBean> matchingBeans = getMatchingBeans(module, it, alreadyMapped);
|
||||
|
||||
result.addAll(it, matchingBeans);
|
||||
alreadyMapped.addAll(matchingBeans);
|
||||
});
|
||||
|
||||
// Wipe entries without any beans
|
||||
new HashSet<>(result.keySet()).forEach(key -> {
|
||||
if (result.get(key).isEmpty()) {
|
||||
result.remove(key);
|
||||
}
|
||||
});
|
||||
|
||||
return Groupings.of(result);
|
||||
}
|
||||
|
||||
private Optional<String> getTargetFileName() {
|
||||
return Optional.ofNullable(targetFileName);
|
||||
}
|
||||
|
||||
private static List<SpringBean> getMatchingBeans(Module module, Grouping filter, List<SpringBean> alreadyMapped) {
|
||||
|
||||
return module.getSpringBeans().stream()
|
||||
.filter(it -> !alreadyMapped.contains(it))
|
||||
.filter(filter::matches)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Value(staticConstructor = "of")
|
||||
@Getter(AccessLevel.PACKAGE)
|
||||
public static class Grouping {
|
||||
|
||||
String name;
|
||||
@Nullable String description;
|
||||
Predicate<SpringBean> predicate;
|
||||
|
||||
public static Grouping of(String name) {
|
||||
return new Grouping(name, null, __ -> false);
|
||||
}
|
||||
|
||||
public static Grouping of(String name, Predicate<SpringBean> predicate) {
|
||||
return new Grouping(name, null, predicate);
|
||||
}
|
||||
|
||||
public boolean matches(SpringBean candidate) {
|
||||
return predicate.test(candidate);
|
||||
}
|
||||
|
||||
public static Predicate<SpringBean> nameMatching(String pattern) {
|
||||
return bean -> bean.getFullyQualifiedTypeName().matches(pattern);
|
||||
}
|
||||
|
||||
public static Predicate<SpringBean> implementing(Class<?> type) {
|
||||
return bean -> bean.getType().isAssignableTo(type);
|
||||
}
|
||||
|
||||
public static Predicate<SpringBean> subtypeOf(Class<?> type) {
|
||||
return implementing(type) //
|
||||
.and(bean -> !bean.getType().isEquivalentTo(type));
|
||||
}
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor(access = AccessLevel.PACKAGE, staticName = "of")
|
||||
static class Groupings {
|
||||
|
||||
private final MultiValueMap<Grouping, SpringBean> groupings;
|
||||
|
||||
Set<Grouping> keySet() {
|
||||
return groupings.keySet();
|
||||
}
|
||||
|
||||
List<SpringBean> byGrouping(Grouping grouping) {
|
||||
return byFilter(grouping::equals);
|
||||
}
|
||||
|
||||
List<SpringBean> byGroupName(String name) {
|
||||
return byFilter(it -> it.getName().equals(name));
|
||||
}
|
||||
|
||||
void forEach(BiConsumer<Grouping, List<SpringBean>> consumer) {
|
||||
groupings.forEach(consumer);
|
||||
}
|
||||
|
||||
private List<SpringBean> byFilter(Predicate<Grouping> filter) {
|
||||
|
||||
return groupings.entrySet().stream()
|
||||
.filter(it -> filter.test(it.getKey()))
|
||||
.findFirst()
|
||||
.map(Entry::getValue)
|
||||
.orElseGet(Collections::emptyList);
|
||||
}
|
||||
|
||||
boolean hasOnlyFallbackGroup() {
|
||||
return groupings.size() == 1 && groupings.get(FALLBACK_GROUP) != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.docs;
|
||||
|
||||
import capital.scalable.restdocs.javadoc.JavadocReader;
|
||||
import capital.scalable.restdocs.javadoc.JavadocReaderImpl;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import com.tngtech.archunit.core.domain.JavaMethod;
|
||||
|
||||
/**
|
||||
* A {@link DocumentationSource} that uses metadata generated by Spring Auto REST Docs' Javadoc Doclet.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 1.1
|
||||
*/
|
||||
class SpringAutoRestDocsDocumentationSource implements DocumentationSource {
|
||||
|
||||
private final JavadocReader reader = JavadocReaderImpl.createWithSystemProperty();
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.moduliths.docs.JavadocSource#getDocumentation(com.tngtech.archunit.core.domain.JavaMethod)
|
||||
*/
|
||||
@Override
|
||||
public Optional<String> getDocumentation(JavaMethod method) {
|
||||
return Optional.of(reader.resolveMethodComment(method.getOwner().reflect(), method.getName()))
|
||||
.filter(it -> !it.isEmpty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
@org.springframework.lang.NonNullApi
|
||||
package org.moduliths.docs;
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.docs;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.moduliths.model.Modules;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
import com.tngtech.archunit.core.importer.ClassFileImporter;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
class AsciidoctorUnitTests {
|
||||
|
||||
Asciidoctor asciidoctor = Asciidoctor.withJavadocBase(Modules.of("org.moduliths"), "{javadoc}");
|
||||
|
||||
@Test
|
||||
void formatsInlineCode() {
|
||||
assertThat(asciidoctor.toInlineCode("Foo")).isEqualTo("`Foo`");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rendersLinkToMethodReference() {
|
||||
|
||||
assertThat(asciidoctor.toInlineCode("Documenter#toModuleCanvas(Module, CanvasOptions)"))
|
||||
.isEqualTo("link:{javadoc}/org/moduliths/docs/Documenter.html"
|
||||
+ "[`o.m.d.Documenter#toModuleCanvas(Module, CanvasOptions)`]");
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotRenderLinkToMethodReferenceForNonPublicType() {
|
||||
|
||||
assertThat(asciidoctor.toInlineCode("DocumentationSource#getDocumentation(JavaMethod)"))
|
||||
.isEqualTo("`o.m.d.DocumentationSource#getDocumentation(JavaMethod)`");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rendersInlineCodeForNonModuleTypeCorrectly() {
|
||||
|
||||
JavaClass type = new ClassFileImporter().importClass(ApplicationContext.class);
|
||||
|
||||
assertThatCode(() -> asciidoctor.toInlineCode(type)).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
void cleansUpJavadocForConfigurationProperties() {
|
||||
|
||||
ConfigurationProperties metadata = new ConfigurationProperties();
|
||||
|
||||
assertThat(metadata).containsExactly(new ConfigurationProperties.ConfigurationProperty("org.moduliths.sample.test",
|
||||
"Some test property of type {@link java.lang.Boolean}.", "java.lang.Boolean",
|
||||
"com.acme.myproject.stereotypes.Stereotypes$SomeConfigurationProperties", "false"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.docs;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Comparator;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.moduliths.docs.Documenter.Options;
|
||||
import org.moduliths.model.Module;
|
||||
import org.moduliths.model.Module.DependencyType;
|
||||
|
||||
import com.acme.myproject.Application;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link Documenter}.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
class DocumenterTest {
|
||||
|
||||
Documenter documenter = new Documenter(Application.class);
|
||||
|
||||
@Test
|
||||
void writesComponentStructureAsPlantUml() throws IOException {
|
||||
documenter.toPlantUml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void writesSingleModuleDocumentation() throws IOException {
|
||||
|
||||
Module module = documenter.getModules().getModuleByName("moduleB") //
|
||||
.orElseThrow(() -> new IllegalArgumentException());
|
||||
|
||||
documenter.writeModuleAsPlantUml(module, Options.defaults() //
|
||||
.withColorSelector(it -> Optional.of("#ff0000")) //
|
||||
.withDefaultDisplayName(it -> it.getDisplayName().toUpperCase()));
|
||||
|
||||
Options options = Options.defaults() //
|
||||
.withComponentFilter(component -> component.getRelationships().stream()
|
||||
.anyMatch(it -> it.getTagsAsSet().contains(DependencyType.EVENT_LISTENER.toString())));
|
||||
|
||||
documenter.writeModulesAsPlantUml(options);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testName() {
|
||||
|
||||
documenter.getModules().stream() //
|
||||
.map(it -> documenter.toModuleCanvas(it));
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizesOutputLocation() throws IOException {
|
||||
|
||||
String customOutputFolder = "build/moduliths";
|
||||
Path path = Paths.get(customOutputFolder);
|
||||
|
||||
try {
|
||||
|
||||
documenter.withOutputFolder(customOutputFolder).writeModuleCanvases();
|
||||
|
||||
assertThat(Files.list(path)).isNotEmpty();
|
||||
assertThat(path).exists();
|
||||
|
||||
} finally {
|
||||
|
||||
Files.walk(path)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
.map(Path::toFile)
|
||||
.forEach(File::delete);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"groups": [
|
||||
{
|
||||
"name": "org.moduliths.sample",
|
||||
"type": "com.acme.myproject.stereotypes.Stereotypes$SomeConfigurationProperties",
|
||||
"sourceType": "com.acme.myproject.stereotypes.Stereotypes$SomeConfigurationProperties"
|
||||
}
|
||||
],
|
||||
"properties": [
|
||||
{
|
||||
"name": "org.moduliths.sample.test",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Some test property of type {@link java.lang.Boolean}.",
|
||||
"sourceType": "com.acme.myproject.stereotypes.Stereotypes$SomeConfigurationProperties",
|
||||
"defaultValue": false
|
||||
}
|
||||
],
|
||||
"hints": []
|
||||
}
|
||||
14
moduliths-docs/src/test/resources/logback.xml
Normal file
14
moduliths-docs/src/test/resources/logback.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
|
||||
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d %5p %40.40c:%4L - %m%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="error">
|
||||
<appender-ref ref="console" />
|
||||
</root>
|
||||
|
||||
</configuration>
|
||||
20
moduliths-events/moduliths-events-core/pom.xml
Normal file
20
moduliths-events/moduliths-events-core/pom.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.moduliths</groupId>
|
||||
<artifactId>moduliths-events</artifactId>
|
||||
<version>1.4.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>Moduliths - Events - Core</name>
|
||||
|
||||
<artifactId>moduliths-events-core</artifactId>
|
||||
|
||||
<properties>
|
||||
<module.name>org.moduliths.events.core</module.name>
|
||||
</properties>
|
||||
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2017-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.events;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* An event publication that can be completed.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public interface CompletableEventPublication extends EventPublication {
|
||||
|
||||
/**
|
||||
* Returns the completion date of the publication.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
Optional<Instant> getCompletionDate();
|
||||
|
||||
/**
|
||||
* Returns whether the publication o
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
default boolean isPublicationCompleted() {
|
||||
return getCompletionDate().isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the event publication as completed.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
CompletableEventPublication markCompleted();
|
||||
|
||||
/**
|
||||
* Creates a {@link CompletableEventPublication} for the given event an listener identifier.
|
||||
*
|
||||
* @param event must not be {@literal null}.
|
||||
* @param id must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
static CompletableEventPublication of(Object event, PublicationTargetIdentifier id) {
|
||||
return DefaultEventPublication.of(event, id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.events;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Default {@link CompletableEventPublication} implementation.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor(staticName = "of")
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
class DefaultEventPublication implements CompletableEventPublication {
|
||||
|
||||
private final @NonNull Object event;
|
||||
private final @NonNull PublicationTargetIdentifier targetIdentifier;
|
||||
private final Instant publicationDate = Instant.now();
|
||||
|
||||
private Optional<Instant> completionDate = Optional.empty();
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see de.olivergierke.events.CompletableEventPublication#markCompleted()
|
||||
*/
|
||||
@Override
|
||||
public CompletableEventPublication markCompleted() {
|
||||
|
||||
this.completionDate = Optional.of(Instant.now());
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2017-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.events;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.PayloadApplicationEvent;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An event publication.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @see CompletableEventPublication#of(Object, PublicationTargetIdentifier)
|
||||
*/
|
||||
public interface EventPublication extends Comparable<EventPublication> {
|
||||
|
||||
/**
|
||||
* Returns the event that is published.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
Object getEvent();
|
||||
|
||||
/**
|
||||
* Returns the event as Spring {@link ApplicationEvent}, effectively wrapping it into a
|
||||
* {@link PayloadApplicationEvent} in case it's not one already.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
default ApplicationEvent getApplicationEvent() {
|
||||
|
||||
Object event = getEvent();
|
||||
|
||||
return PayloadApplicationEvent.class.isInstance(event) //
|
||||
? PayloadApplicationEvent.class.cast(event)
|
||||
: new PayloadApplicationEvent<>(this, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the time the event is published at.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
Instant getPublicationDate();
|
||||
|
||||
/**
|
||||
* Returns the identifier of the target that the event is supposed to be published to.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
PublicationTargetIdentifier getTargetIdentifier();
|
||||
|
||||
/**
|
||||
* Returns whether the publication is identified by the given {@link PublicationTargetIdentifier}.
|
||||
*
|
||||
* @param identifier must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
default boolean isIdentifiedBy(PublicationTargetIdentifier identifier) {
|
||||
|
||||
Assert.notNull(identifier, "Identifier must not be null!");
|
||||
|
||||
return this.getTargetIdentifier().equals(identifier);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Comparable#compareTo(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public default int compareTo(EventPublication that) {
|
||||
return this.getPublicationDate().compareTo(that.getPublicationDate());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2017-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.events;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A registry to capture event publications to {@link ApplicationListener}s. Allows to register those publications, mark
|
||||
* them as completed and lookup incomplete publications.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public interface EventPublicationRegistry {
|
||||
|
||||
/**
|
||||
* Stores {@link EventPublication}s for the given event and {@link ApplicationListener}s.
|
||||
*
|
||||
* @param event must not be {@literal null}.
|
||||
* @param listeners must not be {@literal null}.
|
||||
*/
|
||||
void store(Object event, Stream<PublicationTargetIdentifier> listeners);
|
||||
|
||||
/**
|
||||
* Marks the publication for the given event and {@link PublicationTargetIdentifier} as completed.
|
||||
*
|
||||
* @param event must not be {@literal null}.
|
||||
* @param listener must not be {@literal null}.
|
||||
*/
|
||||
void markCompleted(Object event, PublicationTargetIdentifier listener);
|
||||
|
||||
/**
|
||||
* Marks the given {@link EventPublication} as completed.
|
||||
*
|
||||
* @param publication must not be {@literal null}.
|
||||
*/
|
||||
default void markCompleted(EventPublication publication) {
|
||||
|
||||
Assert.notNull(publication, "Publication must not be null!");
|
||||
|
||||
markCompleted(publication.getEvent(), publication.getTargetIdentifier());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all {@link EventPublication}s that have not been completed yet.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
Iterable<EventPublication> findIncompletePublications();
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2017-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.events;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public interface EventSerializer {
|
||||
|
||||
/**
|
||||
* Serializes the given event into a storable format.
|
||||
*
|
||||
* @param event must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
Object serialize(Object event);
|
||||
|
||||
/**
|
||||
* Deserializes the event from the serialization format into an instance of the given type.
|
||||
*
|
||||
* @param serialized must not be {@literal null}.
|
||||
* @param type must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
Object deserialize(Object serialized, Class<?> type);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2017-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.events;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Value;
|
||||
|
||||
/**
|
||||
* Identifier for a publication target.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@Value
|
||||
@RequiredArgsConstructor(staticName = "of")
|
||||
public class PublicationTargetIdentifier {
|
||||
|
||||
String value;
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.events.config;
|
||||
|
||||
import static org.springframework.core.io.support.SpringFactoriesLoader.*;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.moduliths.events.config.EnablePersistentDomainEvents.PersistentDomainEventsImportSelector;
|
||||
import org.springframework.context.ResourceLoaderAware;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.ImportSelector;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Inherited
|
||||
@Import(PersistentDomainEventsImportSelector.class)
|
||||
public @interface EnablePersistentDomainEvents {
|
||||
|
||||
@RequiredArgsConstructor
|
||||
static class PersistentDomainEventsImportSelector implements ImportSelector, ResourceLoaderAware {
|
||||
|
||||
private ResourceLoader resourceLoader;
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.context.ResourceLoaderAware#setResourceLoader(org.springframework.core.io.ResourceLoader)
|
||||
*/
|
||||
@Override
|
||||
public void setResourceLoader(ResourceLoader resourceLoader) {
|
||||
this.resourceLoader = resourceLoader;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.context.annotation.ImportSelector#selectImports(org.springframework.core.type.AnnotationMetadata)
|
||||
*/
|
||||
@Override
|
||||
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
|
||||
|
||||
List<String> result = new ArrayList<>();
|
||||
|
||||
result.add(EventPublicationConfiguration.class.getName());
|
||||
result.addAll(loadFactoryNames(EventPublicationConfigurationExtension.class, resourceLoader.getClassLoader()));
|
||||
result.addAll(loadFactoryNames(EventSerializationConfigurationExtension.class, resourceLoader.getClassLoader()));
|
||||
|
||||
return result.toArray(new String[result.size()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2017-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.events.config;
|
||||
|
||||
import org.moduliths.events.EventPublicationRegistry;
|
||||
import org.moduliths.events.support.CompletionRegisteringBeanPostProcessor;
|
||||
import org.moduliths.events.support.MapEventPublicationRegistry;
|
||||
import org.moduliths.events.support.PersistentApplicationEventMulticaster;
|
||||
import org.springframework.beans.factory.ObjectFactory;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class EventPublicationConfiguration {
|
||||
|
||||
@Bean
|
||||
PersistentApplicationEventMulticaster applicationEventMulticaster(ObjectProvider<EventPublicationRegistry> registry) {
|
||||
|
||||
return new PersistentApplicationEventMulticaster(
|
||||
() -> registry.getIfAvailable(() -> new MapEventPublicationRegistry()));
|
||||
}
|
||||
|
||||
@Bean
|
||||
static CompletionRegisteringBeanPostProcessor bpp(ObjectFactory<EventPublicationRegistry> store) {
|
||||
return new CompletionRegisteringBeanPostProcessor(() -> store.getObject());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.events.config;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public interface EventPublicationConfigurationExtension {}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.events.config;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public interface EventSerializationConfigurationExtension {}
|
||||
@@ -0,0 +1,2 @@
|
||||
@org.springframework.lang.NonNullApi
|
||||
package org.moduliths.events.config;
|
||||
@@ -0,0 +1,2 @@
|
||||
@org.springframework.lang.NonNullApi
|
||||
package org.moduliths.events;
|
||||
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* Copyright 2017-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.events.support;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Value;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.aopalliance.aop.Advice;
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.moduliths.events.EventPublicationRegistry;
|
||||
import org.moduliths.events.PublicationTargetIdentifier;
|
||||
import org.springframework.aop.framework.Advised;
|
||||
import org.springframework.aop.framework.AopProxyUtils;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.transaction.event.TransactionPhase;
|
||||
import org.springframework.transaction.event.TransactionalApplicationListenerMethodAdapter;
|
||||
import org.springframework.transaction.event.TransactionalEventListener;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ConcurrentLruCache;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.ReflectionUtils.MethodCallback;
|
||||
|
||||
/**
|
||||
* {@link BeanPostProcessor} that will add a
|
||||
* {@link org.moduliths.events.support.CompletionRegisteringBeanPostProcessor.ProxyCreatingMethodCallback.CompletionRegisteringMethodInterceptor}
|
||||
* to the bean in case it carries a {@link TransactionalEventListener} annotation so that the successful invocation of
|
||||
* those methods mark the event publication to those listeners as completed.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class CompletionRegisteringBeanPostProcessor implements BeanPostProcessor {
|
||||
|
||||
private final Supplier<EventPublicationRegistry> registry;
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization(java.lang.Object, java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
|
||||
ProxyCreatingMethodCallback callback = new ProxyCreatingMethodCallback(registry, beanName, bean, false);
|
||||
|
||||
ReflectionUtils.doWithMethods(AopProxyUtils.ultimateTargetClass(bean), callback);
|
||||
|
||||
return callback.methodFound ? callback.getBean() : bean;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Method callback to find a {@link TransactionalEventListener} method and creating a proxy including an
|
||||
* {@link CompletionRegisteringBeanPostProcessor} for it or adding the latter to the already existing advisor chain.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
private static class ProxyCreatingMethodCallback implements MethodCallback {
|
||||
|
||||
private @NonNull final Supplier<EventPublicationRegistry> registry;
|
||||
private @NonNull final String beanName;
|
||||
private @NonNull @Getter Object bean;
|
||||
private boolean methodFound;
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.util.ReflectionUtils.MethodCallback#doWith(java.lang.reflect.Method)
|
||||
*/
|
||||
@Override
|
||||
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
|
||||
|
||||
if (methodFound || !CompletionRegisteringMethodInterceptor.isCompletingMethod(method)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.methodFound = true;
|
||||
this.bean = createCompletionRegisteringProxy(bean,
|
||||
new CompletionRegisteringMethodInterceptor(registry, beanName));
|
||||
}
|
||||
|
||||
private static Object createCompletionRegisteringProxy(Object bean, Advice interceptor) {
|
||||
|
||||
if (bean instanceof Advised) {
|
||||
|
||||
Advised advised = (Advised) bean;
|
||||
advised.addAdvice(advised.getAdvisors().length, interceptor);
|
||||
|
||||
return bean;
|
||||
}
|
||||
|
||||
ProxyFactory factory = new ProxyFactory(bean);
|
||||
factory.setProxyTargetClass(true);
|
||||
factory.addAdvice(interceptor);
|
||||
|
||||
return factory.getProxy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link MethodInterceptor} to trigger the completion of an event publication after a transaction event listener
|
||||
* method has been completed successfully.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
private static class CompletionRegisteringMethodInterceptor implements MethodInterceptor, Ordered {
|
||||
|
||||
private static final ConcurrentLruCache<Method, Boolean> COMPLETING_METHOD = new ConcurrentLruCache<>(100,
|
||||
CompletionRegisteringMethodInterceptor::calculateIsCompletingMethod);
|
||||
private static final ConcurrentLruCache<CacheKey, TransactionalApplicationListenerMethodAdapter> ADAPTERS = new ConcurrentLruCache<>(
|
||||
100, CompletionRegisteringMethodInterceptor::createAdapter);
|
||||
|
||||
private final @NonNull Supplier<EventPublicationRegistry> registry;
|
||||
private final @NonNull String beanName;
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
|
||||
*/
|
||||
@Override
|
||||
public Object invoke(MethodInvocation invocation) throws Throwable {
|
||||
|
||||
Object result = null;
|
||||
Method method = invocation.getMethod();
|
||||
|
||||
try {
|
||||
result = invocation.proceed();
|
||||
} catch (Exception o_O) {
|
||||
|
||||
if (!isCompletingMethod(method)) {
|
||||
throw o_O;
|
||||
}
|
||||
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Invocation of listener {} failed. Leaving event publication uncompleted.", method, o_O);
|
||||
} else {
|
||||
LOG.info("Invocation of listener {} failed with message {}. Leaving event publication uncompleted.",
|
||||
method, o_O.getMessage());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!isCompletingMethod(method)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Mark publication complete if the method is a transactional event listener.
|
||||
String adapterId = ADAPTERS.get(CacheKey.of(beanName, method)).getListenerId();
|
||||
PublicationTargetIdentifier identifier = PublicationTargetIdentifier.of(adapterId);
|
||||
registry.get().markCompleted(invocation.getArguments()[0], identifier);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.core.Ordered#getOrder()
|
||||
*/
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Ordered.HIGHEST_PRECEDENCE - 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given method is one that requires publication completion.
|
||||
*
|
||||
* @param method must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
static boolean isCompletingMethod(Method method) {
|
||||
|
||||
Assert.notNull(method, "Method must not be null!");
|
||||
|
||||
return COMPLETING_METHOD.get(method);
|
||||
}
|
||||
|
||||
private static boolean calculateIsCompletingMethod(Method method) {
|
||||
|
||||
TransactionalEventListener annotation = AnnotatedElementUtils.getMergedAnnotation(method,
|
||||
TransactionalEventListener.class);
|
||||
|
||||
return annotation == null ? false : annotation.phase().equals(TransactionPhase.AFTER_COMMIT);
|
||||
}
|
||||
|
||||
private static TransactionalApplicationListenerMethodAdapter createAdapter(CacheKey key) {
|
||||
return new TransactionalApplicationListenerMethodAdapter(key.beanName, key.method.getDeclaringClass(),
|
||||
key.method);
|
||||
}
|
||||
}
|
||||
|
||||
@Value(staticConstructor = "of")
|
||||
static class CacheKey {
|
||||
|
||||
String beanName;
|
||||
Method method;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2017-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.events.support;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.moduliths.events.CompletableEventPublication;
|
||||
import org.moduliths.events.EventPublication;
|
||||
import org.moduliths.events.EventPublicationRegistry;
|
||||
import org.moduliths.events.PublicationTargetIdentifier;
|
||||
|
||||
/**
|
||||
* Map based {@link EventPublicationRegistry}, for testing purposes only.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public class MapEventPublicationRegistry implements EventPublicationRegistry {
|
||||
|
||||
private final Map<Key, CompletableEventPublication> events = new HashMap<>();
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.events.EventPublicationRegistry#findIncompletePublications()
|
||||
*/
|
||||
@Override
|
||||
public Iterable<EventPublication> findIncompletePublications() {
|
||||
|
||||
return events.entrySet().stream()//
|
||||
.filter(it -> !it.getValue().isPublicationCompleted())//
|
||||
.map(it -> it.getValue())//
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.events.EventPublicationRegistry#store(java.lang.Object, java.util.Collection)
|
||||
*/
|
||||
@Override
|
||||
public void store(Object event, Stream<PublicationTargetIdentifier> identifiers) {
|
||||
|
||||
identifiers.forEach(id -> {
|
||||
events.computeIfAbsent(Key.of(event, id), it -> CompletableEventPublication.of(event, id));
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.events.EventPublicationRegistry#markCompleted(java.lang.Object, org.springframework.events.PublicationTargetIdentifier)
|
||||
*/
|
||||
@Override
|
||||
public void markCompleted(Object event, PublicationTargetIdentifier id) {
|
||||
events.computeIfPresent(Key.of(event, id), (__, value) -> value.markCompleted());
|
||||
}
|
||||
|
||||
@Value(staticConstructor = "of")
|
||||
private static class Key {
|
||||
|
||||
Object event;
|
||||
PublicationTargetIdentifier identifier;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
* Copyright 2017-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.events.support;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.moduliths.events.EventPublication;
|
||||
import org.moduliths.events.EventPublicationRegistry;
|
||||
import org.moduliths.events.PublicationTargetIdentifier;
|
||||
import org.springframework.beans.factory.SmartInitializingSingleton;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.PayloadApplicationEvent;
|
||||
import org.springframework.context.event.AbstractApplicationEventMulticaster;
|
||||
import org.springframework.context.event.ApplicationEventMulticaster;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.transaction.event.TransactionPhase;
|
||||
import org.springframework.transaction.event.TransactionalApplicationListener;
|
||||
import org.springframework.transaction.event.TransactionalEventListener;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link ApplicationEventMulticaster} to register {@link EventPublication}s in an {@link EventPublicationRegistry}
|
||||
* so that potentially failing transactional event listeners can get re-invoked upon application restart or via a
|
||||
* schedule.
|
||||
* <p>
|
||||
* Republication is handled in {@link #afterSingletonsInstantiated()} inspecting the {@link EventPublicationRegistry}
|
||||
* for incomplete publications and
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @see CompletionRegisteringBeanPostProcessor
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class PersistentApplicationEventMulticaster extends AbstractApplicationEventMulticaster
|
||||
implements SmartInitializingSingleton {
|
||||
|
||||
private final @NonNull Supplier<EventPublicationRegistry> registry;
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.context.event.ApplicationEventMulticaster#multicastEvent(org.springframework.context.ApplicationEvent)
|
||||
*/
|
||||
@Override
|
||||
public void multicastEvent(ApplicationEvent event) {
|
||||
multicastEvent(event, ResolvableType.forInstance(event));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.context.event.ApplicationEventMulticaster#multicastEvent(org.springframework.context.ApplicationEvent, org.springframework.core.ResolvableType)
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public void multicastEvent(ApplicationEvent event, ResolvableType eventType) {
|
||||
|
||||
ResolvableType type = eventType == null ? ResolvableType.forInstance(event) : eventType;
|
||||
Collection<ApplicationListener<?>> listeners = getApplicationListeners(event, type);
|
||||
|
||||
if (listeners.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
TransactionalEventListeners txListeners = new TransactionalEventListeners(listeners);
|
||||
Object eventToPersist = getEventToPersist(event);
|
||||
registry.get().store(eventToPersist, txListeners.stream() //
|
||||
.map(TransactionalApplicationListener::getListenerId) //
|
||||
.map(PublicationTargetIdentifier::of));
|
||||
|
||||
for (ApplicationListener listener : listeners) {
|
||||
listener.onApplicationEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.beans.factory.SmartInitializingSingleton#afterSingletonsInstantiated()
|
||||
*/
|
||||
@Override
|
||||
public void afterSingletonsInstantiated() {
|
||||
|
||||
for (EventPublication publication : registry.get().findIncompletePublications()) {
|
||||
invokeTargetListener(publication);
|
||||
}
|
||||
}
|
||||
|
||||
private void invokeTargetListener(EventPublication publication) {
|
||||
|
||||
TransactionalEventListeners listeners = new TransactionalEventListeners(
|
||||
getApplicationListeners());
|
||||
|
||||
listeners.stream() //
|
||||
.filter(it -> publication.isIdentifiedBy(PublicationTargetIdentifier.of(it.getListenerId()))) //
|
||||
.findFirst() //
|
||||
.map(it -> executeListenerWithCompletion(publication, it)) //
|
||||
.orElseGet(() -> {
|
||||
|
||||
LOG.debug("Listener {} not found!", publication.getTargetIdentifier());
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private ApplicationListener<ApplicationEvent> executeListenerWithCompletion(EventPublication publication,
|
||||
TransactionalApplicationListener<ApplicationEvent> listener) {
|
||||
|
||||
listener.processEvent(publication.getApplicationEvent());
|
||||
|
||||
return listener;
|
||||
}
|
||||
|
||||
private static Object getEventToPersist(ApplicationEvent event) {
|
||||
|
||||
return PayloadApplicationEvent.class.isInstance(event) //
|
||||
? ((PayloadApplicationEvent<?>) event).getPayload() //
|
||||
: event;
|
||||
}
|
||||
|
||||
/**
|
||||
* First-class collection to work with transactional event listeners, i.e. {@link ApplicationListener} instances that
|
||||
* implement {@link TransactionalEventListenerMetadata}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 1.1
|
||||
* @see TransactionalEventListener
|
||||
* @see TransactionalEventListenerMetadata
|
||||
*/
|
||||
static class TransactionalEventListeners {
|
||||
|
||||
private final List<TransactionalApplicationListener<ApplicationEvent>> listeners;
|
||||
|
||||
/**
|
||||
* Creates a new {@link TransactionalEventListeners} instance by filtering all elements implementing
|
||||
* {@link TransactionalEventListenerMetadata}.
|
||||
*
|
||||
* @param listeners must not be {@literal null}.
|
||||
*/
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public TransactionalEventListeners(Collection<ApplicationListener<?>> listeners) {
|
||||
|
||||
Assert.notNull(listeners, "ApplicationListeners must not be null!");
|
||||
|
||||
this.listeners = (List) listeners.stream()
|
||||
.filter(TransactionalApplicationListener.class::isInstance)
|
||||
.map(TransactionalApplicationListener.class::cast)
|
||||
.sorted(AnnotationAwareOrderComparator.INSTANCE)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private TransactionalEventListeners(
|
||||
List<TransactionalApplicationListener<ApplicationEvent>> listeners) {
|
||||
this.listeners = listeners;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all {@link TransactionalEventListeners} for the given {@link TransactionPhase}.
|
||||
*
|
||||
* @param phase must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public TransactionalEventListeners forPhase(TransactionPhase phase) {
|
||||
|
||||
Assert.notNull(phase, "TransactionPhase must not be null!");
|
||||
|
||||
List<TransactionalApplicationListener<ApplicationEvent>> collect = listeners.stream()
|
||||
.filter(it -> it.getTransactionPhase().equals(phase))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return new TransactionalEventListeners(collect);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the given {@link Consumer} for all transactional event listeners.
|
||||
*
|
||||
* @param callback must not be {@literal null}.
|
||||
*/
|
||||
public void forEach(Consumer<TransactionalApplicationListener<?>> callback) {
|
||||
|
||||
Assert.notNull(callback, "Callback must not be null!");
|
||||
|
||||
listeners.forEach(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given consumer only if there are actual listeners available.
|
||||
*
|
||||
* @param metadata must not be {@literal null}.
|
||||
*/
|
||||
public void ifPresent(Consumer<Stream<TransactionalApplicationListener<ApplicationEvent>>> metadata) {
|
||||
|
||||
Assert.notNull(metadata, "Callback must not be null!");
|
||||
|
||||
if (!listeners.isEmpty()) {
|
||||
metadata.accept(listeners.stream());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all transactional event listeners.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public Stream<TransactionalApplicationListener<ApplicationEvent>> stream() {
|
||||
return listeners.stream();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the given {@link Consumer} for the listener with the given identifier.
|
||||
*
|
||||
* @param identifier must not be {@literal null} or empty.
|
||||
* @param callback must not be {@literal null}.
|
||||
*/
|
||||
public void doWithListener(String identifier,
|
||||
Consumer<TransactionalApplicationListener<ApplicationEvent>> callback) {
|
||||
|
||||
Assert.hasText(identifier, "Identifier must not be null or empty!");
|
||||
Assert.notNull(callback, "Callback must not be null!");
|
||||
|
||||
listeners.stream()
|
||||
.filter(it -> it.getListenerId().equals(identifier))
|
||||
.findFirst()
|
||||
.ifPresent(callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
@org.springframework.lang.NonNullApi
|
||||
package org.moduliths.events.support;
|
||||
@@ -0,0 +1,2 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
org.moduliths.events.config.EventPublicationConfiguration
|
||||
@@ -0,0 +1 @@
|
||||
org.moduliths.events.config.EventPublicationConfiguration
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2017-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.events;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
class CompletableEventPublicationUnitTest {
|
||||
|
||||
@Test
|
||||
void rejectsNullEvent() {
|
||||
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)//
|
||||
.isThrownBy(() -> CompletableEventPublication.of(null, PublicationTargetIdentifier.of("foo")))//
|
||||
.withMessageContaining("event");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsNullTargetIdentifier() {
|
||||
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)//
|
||||
.isThrownBy(() -> CompletableEventPublication.of(new Object(), null))//
|
||||
.withMessageContaining("targetIdentifier");
|
||||
}
|
||||
|
||||
@Test
|
||||
void publicationIsIncompleteByDefault() {
|
||||
|
||||
CompletableEventPublication publication = CompletableEventPublication.of(new Object(),
|
||||
PublicationTargetIdentifier.of("foo"));
|
||||
|
||||
assertThat(publication.isPublicationCompleted()).isFalse();
|
||||
assertThat(publication.getCompletionDate()).isNotPresent();
|
||||
}
|
||||
|
||||
@Test
|
||||
void completionCapturesDate() {
|
||||
|
||||
CompletableEventPublication publication = CompletableEventPublication
|
||||
.of(new Object(), PublicationTargetIdentifier.of("foo")).markCompleted();
|
||||
|
||||
assertThat(publication.isPublicationCompleted()).isTrue();
|
||||
assertThat(publication.getCompletionDate()).isPresent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.events.support;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.moduliths.events.EventPublicationRegistry;
|
||||
import org.springframework.aop.framework.Advised;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.transaction.event.TransactionPhase;
|
||||
import org.springframework.transaction.event.TransactionalEventListener;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link CompletionRegisteringBeanPostProcessor}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
class CompletionRegisteringBeanPostProcessorUnitTest {
|
||||
|
||||
EventPublicationRegistry registry = mock(EventPublicationRegistry.class);
|
||||
BeanPostProcessor processor = new CompletionRegisteringBeanPostProcessor(() -> registry);
|
||||
SomeEventListener bean = new SomeEventListener();
|
||||
|
||||
@Test
|
||||
void doesNotProxyNonTransactionalEventListenerClass() {
|
||||
|
||||
NoEventListener bean = new NoEventListener();
|
||||
|
||||
assertThat(bean).isSameAs(processor.postProcessBeforeInitialization(bean, "bean"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void triggersCompletionForAfterCommitEventListener() throws Exception {
|
||||
assertCompletion(SomeEventListener::onAfterCommit);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotTriggerCompletionForNonAfterCommitPhase() throws Exception {
|
||||
assertNonCompletion(SomeEventListener::onAfterRollback);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotTriggerCompletionForPlainEventListener() {
|
||||
assertNonCompletion(SomeEventListener::simpleEventListener);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotTriggerCompletionForNonEventListener() {
|
||||
assertNonCompletion(SomeEventListener::nonEventListener);
|
||||
}
|
||||
|
||||
private void assertCompletion(BiConsumer<SomeEventListener, Object> consumer) {
|
||||
assertCompletion(consumer, true);
|
||||
}
|
||||
|
||||
private void assertNonCompletion(BiConsumer<SomeEventListener, Object> consumer) {
|
||||
assertCompletion(consumer, false);
|
||||
}
|
||||
|
||||
private void assertCompletion(BiConsumer<SomeEventListener, Object> consumer, boolean expected) {
|
||||
|
||||
Object processed = processor.postProcessAfterInitialization(bean, "listener");
|
||||
|
||||
assertThat(processed).isInstanceOf(Advised.class);
|
||||
assertThat(processed).isInstanceOfSatisfying(SomeEventListener.class, //
|
||||
it -> consumer.accept(it, new Object()));
|
||||
|
||||
verify(registry, times(expected ? 1 : 0)).markCompleted(any(), any());
|
||||
}
|
||||
|
||||
static class SomeEventListener {
|
||||
|
||||
@TransactionalEventListener
|
||||
void onAfterCommit(Object event) {}
|
||||
|
||||
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
|
||||
void onAfterRollback(Object object) {}
|
||||
|
||||
@EventListener
|
||||
void simpleEventListener(Object object) {}
|
||||
|
||||
void nonEventListener(Object object) {}
|
||||
}
|
||||
|
||||
static class NoEventListener {}
|
||||
}
|
||||
36
moduliths-events/moduliths-events-jackson/pom.xml
Normal file
36
moduliths-events/moduliths-events-jackson/pom.xml
Normal file
@@ -0,0 +1,36 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.moduliths</groupId>
|
||||
<artifactId>moduliths-events</artifactId>
|
||||
<version>1.4.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>Moduliths - Events - Jackson serializer</name>
|
||||
<artifactId>moduliths-events-jackson</artifactId>
|
||||
|
||||
<properties>
|
||||
<module.name>org.moduliths.events.jackson</module.name>
|
||||
</properties>
|
||||
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.moduliths</groupId>
|
||||
<artifactId>moduliths-events-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Jackson -->
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.events.jackson;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.moduliths.events.config.EventSerializationConfigurationExtension;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@RequiredArgsConstructor
|
||||
class JacksonEventSerializationConfiguration implements EventSerializationConfigurationExtension {
|
||||
|
||||
private final ObjectProvider<ObjectMapper> mapper;
|
||||
|
||||
@Bean
|
||||
public JacksonEventSerializer jacksonEventSerializer() {
|
||||
return new JacksonEventSerializer(() -> mapper.getIfAvailable(() -> defaultObjectMapper()));
|
||||
}
|
||||
|
||||
private static ObjectMapper defaultObjectMapper() {
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
|
||||
|
||||
return mapper;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.events.jackson;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.moduliths.events.EventSerializer;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
class JacksonEventSerializer implements EventSerializer {
|
||||
|
||||
private final Supplier<ObjectMapper> mapper;
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see de.olivergierke.events.EventSerializer#serialize(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public Object serialize(Object event) {
|
||||
|
||||
try {
|
||||
return mapper.get().writeValueAsString(event);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see de.olivergierke.events.EventSerializer#deserialize(java.lang.Object, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public Object deserialize(Object serialized, Class<?> type) {
|
||||
|
||||
try {
|
||||
return mapper.get().readerFor(type).readValue(serialized.toString());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
@org.springframework.lang.NonNullApi
|
||||
package org.moduliths.events.jackson;
|
||||
@@ -0,0 +1,5 @@
|
||||
org.moduliths.events.config.EventSerializationConfigurationExtension=\
|
||||
org.moduliths.events.jackson.JacksonEventSerializationConfiguration
|
||||
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
org.moduliths.events.jackson.JacksonEventSerializationConfiguration
|
||||
@@ -0,0 +1 @@
|
||||
org.moduliths.events.jackson.JacksonEventSerializationConfiguration
|
||||
96
moduliths-events/moduliths-events-jpa-jakarta/pom.xml
Normal file
96
moduliths-events/moduliths-events-jpa-jakarta/pom.xml
Normal file
@@ -0,0 +1,96 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.moduliths</groupId>
|
||||
<artifactId>moduliths-events</artifactId>
|
||||
<version>1.4.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>Moduliths - Events - Jakarta JPA-based registry</name>
|
||||
<artifactId>moduliths-events-jpa-jakarta</artifactId>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<module.name>org.moduliths.events.jpa.jakarta</module.name>
|
||||
<spring-framework.version>6.0.0-M1</spring-framework.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>moduliths-events-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Jakarta Persistence -->
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.persistence</groupId>
|
||||
<artifactId>jakarta.persistence-api</artifactId>
|
||||
<version>3.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Testing -->
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.xml.bind</groupId>
|
||||
<artifactId>jakarta.xml.bind-api</artifactId>
|
||||
<version>3.0.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hibernate.orm</groupId>
|
||||
<artifactId>hibernate-core</artifactId>
|
||||
<version>6.0.0.CR2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.transaction</groupId>
|
||||
<artifactId>jakarta.transaction-api</artifactId>
|
||||
<version>2.0.1-RC1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-orm</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hsqldb</groupId>
|
||||
<artifactId>hsqldb</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-milestone</id>
|
||||
<url>https://repo.spring.io/milestone</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.events.jpa;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@Data
|
||||
@Entity
|
||||
@NoArgsConstructor(force = true)
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
class JpaEventPublication {
|
||||
|
||||
private final @Id @Column(length = 16) UUID id;
|
||||
private final Instant publicationDate;
|
||||
private final String listenerId;
|
||||
private final String serializedEvent;
|
||||
private final Class<?> eventType;
|
||||
|
||||
private Instant completionDate;
|
||||
|
||||
@Builder
|
||||
static JpaEventPublication of(Instant publicationDate, String listenerId, Object serializedEvent,
|
||||
Class<?> eventType) {
|
||||
return new JpaEventPublication(UUID.randomUUID(), publicationDate, listenerId, serializedEvent.toString(),
|
||||
eventType);
|
||||
}
|
||||
|
||||
JpaEventPublication markCompleted() {
|
||||
|
||||
this.completionDate = Instant.now();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.events.jpa;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurationPackage;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@AutoConfigurationPackage
|
||||
class JpaEventPublicationAutoConfiguration {}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.events.jpa;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
|
||||
import org.moduliths.events.EventSerializer;
|
||||
import org.moduliths.events.config.EventPublicationConfigurationExtension;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@RequiredArgsConstructor
|
||||
class JpaEventPublicationConfiguration implements EventPublicationConfigurationExtension {
|
||||
|
||||
@Bean
|
||||
public JpaEventPublicationRegistry jpaEventPublicationRegistry(JpaEventPublicationRepository repository,
|
||||
EventSerializer serializer) {
|
||||
return new JpaEventPublicationRegistry(repository, serializer);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JpaEventPublicationRepository jpaEventPublicationRepository(EntityManager em) {
|
||||
return new JpaEventPublicationRepository(em);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* Copyright 2017-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.events.jpa;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.moduliths.events.CompletableEventPublication;
|
||||
import org.moduliths.events.EventPublication;
|
||||
import org.moduliths.events.EventPublicationRegistry;
|
||||
import org.moduliths.events.EventSerializer;
|
||||
import org.moduliths.events.PublicationTargetIdentifier;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* JPA based {@link EventPublicationRegistry}.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
class JpaEventPublicationRegistry implements EventPublicationRegistry, DisposableBean {
|
||||
|
||||
private final @NonNull JpaEventPublicationRepository events;
|
||||
private final @NonNull EventSerializer serializer;
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.events.EventPublicationRegistry#store(java.lang.Object, java.util.Collection)
|
||||
*/
|
||||
@Override
|
||||
public void store(Object event, Stream<PublicationTargetIdentifier> listeners) {
|
||||
|
||||
listeners.map(it -> CompletableEventPublication.of(event, it)) //
|
||||
.map(this::map) //
|
||||
.forEach(it -> events.create(it));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.events.EventPublicationRegistry#findIncompletePublications()
|
||||
*/
|
||||
@Override
|
||||
public Iterable<EventPublication> findIncompletePublications() {
|
||||
|
||||
List<EventPublication> result = events.findByCompletionDateIsNull().stream() //
|
||||
.map(it -> JpaEventPublicationAdapter.of(it, serializer)) //
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.events.EventPublicationRegistry#markCompleted(java.lang.Object, org.springframework.events.ListenerId)
|
||||
*/
|
||||
@Override
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public void markCompleted(Object event, PublicationTargetIdentifier listener) {
|
||||
|
||||
Assert.notNull(event, "Domain event must not be null!");
|
||||
Assert.notNull(listener, "Listener identifier must not be null!");
|
||||
|
||||
events.findBySerializedEventAndListenerId(serializer.serialize(event), listener.toString()) //
|
||||
.map(JpaEventPublicationRegistry::LOGCompleted) //
|
||||
.ifPresent(it -> events.update(it.markCompleted()));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.beans.factory.DisposableBean#destroy()
|
||||
*/
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
|
||||
List<JpaEventPublication> publications = events.findByCompletionDateIsNull();
|
||||
|
||||
if (publications.isEmpty()) {
|
||||
|
||||
LOG.info("No publications outstanding!");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.info("Shutting down with the following publications left unfinished:");
|
||||
|
||||
for (int i = 0; i < publications.size(); i++) {
|
||||
|
||||
String prefix = (i + 1) == publications.size() ? "└─" : "├─";
|
||||
JpaEventPublication it = publications.get(i);
|
||||
|
||||
LOG.info("{} {} - {} - {}", prefix, it.getId(), it.getEventType().getName(), it.getListenerId());
|
||||
}
|
||||
}
|
||||
|
||||
private JpaEventPublication map(EventPublication publication) {
|
||||
|
||||
JpaEventPublication result = JpaEventPublication.builder() //
|
||||
.eventType(publication.getEvent().getClass()) //
|
||||
.publicationDate(publication.getPublicationDate()) //
|
||||
.listenerId(publication.getTargetIdentifier().toString()) //
|
||||
.serializedEvent(serializer.serialize(publication.getEvent()).toString()) //
|
||||
.build();
|
||||
|
||||
LOG.debug("Registering publication of {} with id {} for {}.", //
|
||||
result.getEventType(), result.getId(), result.getListenerId());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static JpaEventPublication LOGCompleted(JpaEventPublication publication) {
|
||||
|
||||
LOG.debug("Marking publication of event {} with id {} to listener {} completed.", //
|
||||
publication.getEventType(), publication.getId(), publication.getListenerId());
|
||||
|
||||
return publication;
|
||||
}
|
||||
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor(staticName = "of")
|
||||
static class JpaEventPublicationAdapter implements EventPublication {
|
||||
|
||||
private final JpaEventPublication publication;
|
||||
private final EventSerializer serializer;
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.events.EventPublication#getEvent()
|
||||
*/
|
||||
@Override
|
||||
public Object getEvent() {
|
||||
return serializer.deserialize(publication.getSerializedEvent(), publication.getEventType());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.events.EventPublication#getListenerId()
|
||||
*/
|
||||
@Override
|
||||
public PublicationTargetIdentifier getTargetIdentifier() {
|
||||
return PublicationTargetIdentifier.of(publication.getListenerId());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.events.EventPublication#getPublicationDate()
|
||||
*/
|
||||
@Override
|
||||
public Instant getPublicationDate() {
|
||||
return publication.getPublicationDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moduliths.events.jpa;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.TypedQuery;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Repository to store {@link JpaEventPublication}s.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class JpaEventPublicationRepository {
|
||||
|
||||
private final EntityManager entityManager;
|
||||
|
||||
@Transactional
|
||||
JpaEventPublication create(JpaEventPublication publication) {
|
||||
|
||||
entityManager.persist(publication);
|
||||
return publication;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
JpaEventPublication update(JpaEventPublication publication) {
|
||||
|
||||
entityManager.merge(publication);
|
||||
entityManager.flush();
|
||||
|
||||
return publication;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all {@link JpaEventPublication} that have not been completed yet.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
List<JpaEventPublication> findByCompletionDateIsNull() {
|
||||
|
||||
String query = "select p from JpaEventPublication p where p.completionDate is null";
|
||||
|
||||
return entityManager.createQuery(query, JpaEventPublication.class).getResultList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link JpaEventPublication} for the given serialized event and listener identifier.
|
||||
*
|
||||
* @param event must not be {@literal null}.
|
||||
* @param listenerId must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
Optional<JpaEventPublication> findBySerializedEventAndListenerId(Object event, String listenerId) {
|
||||
|
||||
String query = "select p from JpaEventPublication p where p.serializedEvent = ?1 and p.listenerId = ?2";
|
||||
|
||||
TypedQuery<JpaEventPublication> typedQuery = entityManager.createQuery(query, JpaEventPublication.class)
|
||||
.setParameter(1, event)
|
||||
.setParameter(2, listenerId);
|
||||
|
||||
try {
|
||||
return Optional.of(typedQuery.getSingleResult());
|
||||
} catch (Exception o_O) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
@org.springframework.lang.NonNullApi
|
||||
package org.moduliths.events.jpa;
|
||||
@@ -0,0 +1 @@
|
||||
restart.include.moduliths-events:/moduliths-events-jpa-[\\w-\\.]+\.jar
|
||||
@@ -0,0 +1,6 @@
|
||||
org.moduliths.events.config.EventPublicationConfigurationExtension=\
|
||||
org.moduliths.events.jpa.JpaEventPublicationConfiguration
|
||||
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
org.moduliths.events.jpa.JpaEventPublicationAutoConfiguration,\
|
||||
org.moduliths.events.jpa.JpaEventPublicationConfiguration
|
||||
@@ -0,0 +1,2 @@
|
||||
org.moduliths.events.jpa.JpaEventPublicationAutoConfiguration
|
||||
org.moduliths.events.jpa.JpaEventPublicationConfiguration
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user