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:
Oliver Drotbohm
2022-07-06 12:58:50 +02:00
commit 6c444769d7
236 changed files with 17771 additions and 0 deletions

49
.github/workflows/build.yaml vendored Normal file
View 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
View 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
View 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

Binary file not shown.

2
.mvn/wrapper/maven-wrapper.properties vendored Normal file
View 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
View 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
View File

@@ -0,0 +1,2 @@
changelog:
repository: spring-projects-experimental/spring-modulith

17
etc/ide/README.md Normal file
View 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

View 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>

View 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
View File

@@ -0,0 +1,3 @@
lombok.nonNull.exceptionType = IllegalArgumentException
lombok.log.fieldName = LOG
lombok.addLombokGeneratedAnnotation = true

28
moduliths-api/pom.xml Normal file
View 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>

View 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 {};
}

View 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 {};
}

View 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 {};
}

View File

@@ -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
View 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>

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View 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());
}
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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("."));
}
}

View File

@@ -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 ? "" : "");
}
}

View File

@@ -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();
}
}

View 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));
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}
}

View 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);
}
}
}

View File

@@ -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();
}

View File

@@ -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()));
}
}
}

View File

@@ -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();
}
}

View 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);
}

View File

@@ -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());
}
}

View 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));
}
}

View 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);
}
}
}

View File

@@ -0,0 +1,2 @@
@org.springframework.lang.NonNullApi
package org.moduliths.model;

View File

@@ -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;
}
}

View File

@@ -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 {}
}

View File

@@ -0,0 +1,2 @@
@org.jmolecules.ddd.annotation.Module
package jmolecules;

View File

@@ -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 {
}
}

View File

@@ -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) {}
}
}

View File

@@ -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) {}
}
}

View File

@@ -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);
}
}

View File

@@ -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();
});
}
}

View File

@@ -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 {}
}

View File

@@ -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()));
}
}

View File

@@ -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");
}
}

View File

@@ -0,0 +1 @@
spring.main.banner-mode=OFF

View 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
View 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>

View 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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View 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;
}
}
}
}

View File

@@ -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());
}
}

View File

@@ -0,0 +1,2 @@
@org.springframework.lang.NonNullApi
package org.moduliths.docs;

View File

@@ -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"));
}
}

View File

@@ -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);
}
}
}

View File

@@ -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": []
}

View 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>

View 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>

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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()]);
}
}
}

View File

@@ -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());
}
}

View File

@@ -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 {}

View File

@@ -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 {}

View File

@@ -0,0 +1,2 @@
@org.springframework.lang.NonNullApi
package org.moduliths.events.config;

View File

@@ -0,0 +1,2 @@
@org.springframework.lang.NonNullApi
package org.moduliths.events;

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,2 @@
@org.springframework.lang.NonNullApi
package org.moduliths.events.support;

View File

@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.moduliths.events.config.EventPublicationConfiguration

View File

@@ -0,0 +1 @@
org.moduliths.events.config.EventPublicationConfiguration

View File

@@ -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();
}
}

View File

@@ -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 {}
}

View 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>

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,2 @@
@org.springframework.lang.NonNullApi
package org.moduliths.events.jackson;

View File

@@ -0,0 +1,5 @@
org.moduliths.events.config.EventSerializationConfigurationExtension=\
org.moduliths.events.jackson.JacksonEventSerializationConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.moduliths.events.jackson.JacksonEventSerializationConfiguration

View File

@@ -0,0 +1 @@
org.moduliths.events.jackson.JacksonEventSerializationConfiguration

View 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>

View File

@@ -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;
}
}

View File

@@ -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 {}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,2 @@
@org.springframework.lang.NonNullApi
package org.moduliths.events.jpa;

View File

@@ -0,0 +1 @@
restart.include.moduliths-events:/moduliths-events-jpa-[\\w-\\.]+\.jar

View File

@@ -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

View File

@@ -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