diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc new file mode 100644 index 00000000..2ae60f7d --- /dev/null +++ b/CONTRIBUTING.adoc @@ -0,0 +1,9 @@ += CONTRIBUTING + +To create or resove issues or to contribute code or documentation, please visit the following WIKI pages: + +* for https://github.com/oasp-forge/oasp4j-wiki/wiki/oasp-issue-work[Issue creation and resolution] +* for https://github.com/oasp-forge/oasp4j-wiki/wiki/oasp-code-contributions[Contributions of code] +* for https://github.com/oasp-forge/oasp4j-wiki/wiki/oasp-documentation[Contributions to documentation] + +Thanks for your contribution! diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..fce5e4c5 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + 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 2015-2018 Capgemini SE. + + 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. diff --git a/boms/bom/pom.xml b/boms/bom/pom.xml new file mode 100644 index 00000000..5f07d8a1 --- /dev/null +++ b/boms/bom/pom.xml @@ -0,0 +1,263 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-boms + dev-SNAPSHOT + + com.devonfw.java.boms + devon4j-bom + ${devon4j.version} + pom + ${project.artifactId} + Dependencies (BOM) of the Open Application Standard Platform for Java (devon4j) based on spring boot. + https://devon4j.github.io/ + 2014 + + + UTF-8 + UTF-8 + 3.2.5 + 7.5.1 + bom + + + + + + + com.devonfw.java.boms + devon4j-minimal-bom + ${devon4j.version} + pom + import + + + + + net.sf.m-m-m + mmm-util-core + ${mmm.util.version} + + + net.sf.m-m-m + mmm-util-validation + ${mmm.util.version} + + + net.sf.m-m-m + mmm-util-search + ${mmm.util.version} + + + net.sf.m-m-m + mmm-util-entity + ${mmm.util.version} + + + + javax.annotation + jsr250-api + 1.0 + + + + javax.inject + javax.inject + 1 + + + + javax.annotation + javax.annotation-api + 1.2 + + + + com.google.guava + guava + 17.0 + + + + org.apache.commons + commons-collections4 + 4.1 + + + + org.javamoney + moneta + 0.8 + + + + org.hibernate.javax.persistence + hibernate-jpa-2.1-api + 1.0.0.Final + + + + cglib + cglib + 3.2.5 + + + + org.apache.commons + commons-dbcp2 + 2.0.1 + + + + org.slf4j + slf4j-api + 1.7.7 + + + + javax.servlet + javax.servlet-api + 3.1.0 + provided + + + + javax.servlet + jstl + 1.2 + + + + javax.ws.rs + javax.ws.rs-api + 2.1 + + + + org.apache.cxf + cxf-core + ${cxf.version} + + + org.apache.cxf + cxf-rt-frontend-jaxws + ${cxf.version} + + + org.apache.cxf + cxf-rt-frontend-jaxrs + ${cxf.version} + + + org.apache.cxf + cxf-rt-rs-service-description + ${cxf.version} + + + org.apache.cxf + cxf-rt-transports-http + ${cxf.version} + + + org.apache.cxf + cxf-rt-transports-local + ${cxf.version} + + + org.apache.cxf + cxf-rt-rs-client + ${cxf.version} + + + + org.json + json + 20180130 + + + + com.fasterxml.jackson.jaxrs + jackson-jaxrs-json-provider + 2.4.2 + + + com.fasterxml.jackson.core + jackson-databind + 2.3.3 + + + com.fasterxml.jackson.core + jackson-core + 2.3.3 + + + + net.sf.dozer + dozer + 5.5.1 + + + + ma.glasnost.orika + orika-core + 1.4.6 + + + + javax.validation + validation-api + 1.1.0.Final + + + + + + junit + junit + 4.12 + + + + org.mockito + mockito-core + 1.9.5 + + + + com.github.tomakehurst + wiremock + 1.54 + + + + + javax.el + javax.el-api + 3.0.0 + + + org.glassfish.web + javax.el + 2.2.6 + + + + + org.owasp + security-logging-logback + 1.1.3 + + + + + + diff --git a/boms/minimal/pom.xml b/boms/minimal/pom.xml new file mode 100644 index 00000000..da9dc7ff --- /dev/null +++ b/boms/minimal/pom.xml @@ -0,0 +1,177 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-boms + dev-SNAPSHOT + + com.devonfw.java.boms + devon4j-minimal-bom + ${devon4j.version} + pom + ${project.artifactId} + Dependencies (BOM) of the Open Application Standard Platform for Java (devon4j) without any thirdparty. + https://devon4j.github.io/ + 2014 + + + UTF-8 + UTF-8 + bom + + + + + + + com.devonfw.java.modules + devon4j-test + ${project.version} + + + com.devonfw.java.modules + devon4j-logging + ${project.version} + + + com.devonfw.java.modules + devon4j-basic + ${project.version} + + + com.devonfw.java.modules + devon4j-batch + ${project.version} + + + com.devonfw.java.modules + devon4j-beanmapping + ${project.version} + + + com.devonfw.java.modules + devon4j-configuration + ${project.version} + + + com.devonfw.java.modules + devon4j-security + ${project.version} + + + com.devonfw.java.modules + devon4j-service + ${project.version} + + + com.devonfw.java.modules + devon4j-json + ${project.version} + + + com.devonfw.java.modules + devon4j-rest + ${project.version} + + + com.devonfw.java.modules + devon4j-cxf-client + ${project.version} + + + com.devonfw.java.modules + devon4j-cxf-client-rest + ${project.version} + + + com.devonfw.java.modules + devon4j-cxf-client-ws + ${project.version} + + + com.devonfw.java.modules + devon4j-cxf-server + ${project.version} + + + com.devonfw.java.modules + devon4j-cxf-server-rest + ${project.version} + + + com.devonfw.java.modules + devon4j-cxf-server-ws + ${project.version} + + + com.devonfw.java.modules + devon4j-jpa + ${project.version} + + + com.devonfw.java.modules + devon4j-jpa-basic + ${project.version} + + + com.devonfw.java.modules + devon4j-jpa-spring-data + ${project.version} + + + com.devonfw.java.modules + devon4j-jpa-dao + ${project.version} + + + com.devonfw.java.modules + devon4j-jpa-envers + ${project.version} + + + com.devonfw.java.modules + devon4j-web + ${project.version} + + + + com.devonfw.java.starters + devon4j-starter-cxf-client + ${project.version} + + + com.devonfw.java.starters + devon4j-starter-cxf-client-rest + ${project.version} + + + com.devonfw.java.starters + devon4j-starter-cxf-client-ws + ${project.version} + + + com.devonfw.java.starters + devon4j-starter-cxf-server + ${project.version} + + + com.devonfw.java.starters + devon4j-starter-cxf-server-rest + ${project.version} + + + com.devonfw.java.starters + devon4j-starter-cxf-server-ws + ${project.version} + + + com.devonfw.java.starters + devon4j-starter-spring-data-jpa + ${project.version} + + + + + diff --git a/boms/pom.xml b/boms/pom.xml new file mode 100644 index 00000000..86186823 --- /dev/null +++ b/boms/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j + dev-SNAPSHOT + + devon4j-boms + pom + ${project.artifactId} + Bill of Materials (BOM) for the Open Application Standard Platform for Java (devon4j). + + + minimal + bom + + + diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..28881951 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,3 @@ +# IMPORTANT + +The content of this folder and it's subfolders is **public** under https://oasp.github.io/oasp4j \ No newline at end of file diff --git a/docs/oomph/projects/OASP4J.setup b/docs/oomph/projects/OASP4J.setup new file mode 100644 index 00000000..588fdc01 --- /dev/null +++ b/docs/oomph/projects/OASP4J.setup @@ -0,0 +1,148 @@ + + + + + + The Github user ID + + + + + Choose from the available Github URIs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The OASP4J github project + diff --git a/modules/basic/pom.xml b/modules/basic/pom.xml new file mode 100644 index 00000000..74f5bf04 --- /dev/null +++ b/modules/basic/pom.xml @@ -0,0 +1,28 @@ + + 4.0.0 + + com.devonfw.java.dev + devon4j-modules + dev-SNAPSHOT + + + com.devonfw.java.modules + devon4j-basic + ${devon4j.version} + jar + ${project.artifactId} + Basic code for common usage (such as base classes for transfer objects) of the Open Application Standard Platform for Java (devon4j). + + + + net.sf.m-m-m + mmm-util-entity + + + ${project.groupId} + devon4j-test + test + + + \ No newline at end of file diff --git a/modules/basic/src/main/java/com/devonfw/module/basic/common/api/config/ConfigProperties.java b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/config/ConfigProperties.java new file mode 100644 index 00000000..5f1a945a --- /dev/null +++ b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/config/ConfigProperties.java @@ -0,0 +1,119 @@ +package com.devonfw.module.basic.common.api.config; + +import java.util.Map; +import java.util.Set; + +/** + * Simple abstraction interface for generic access to configuration properties (from spring-boot + * {@code application.properties}). + * + * @since 3.0.0 + */ +public interface ConfigProperties { + + /** The separator charactor '.' for hierarchical keys such as "spring.datasource.url". */ + char KEY_SEPARATOR = '.'; + + /** An immutable instance of {@link ConfigProperties} that is always {@link #isEmpty() empty} */ + ConfigProperties EMPTY = new EmptyConfigProperties(); + + /** + * @return the {@link Set} of the {@link #getChild(String) direct child keys} available in the + * {@link ConfigProperties}-node. + */ + Set getChildKeys(); + + /** + * @param key the {@link #getChildKeys() child key} of the requested configuration value. + * @return the child {@link ConfigProperties}. Will be an {@link #isEmpty() empty} child if undefined. + */ + ConfigProperties getChild(String key); + + /** + * Recursive variant of {@link #getChild(String)} such that + * {@link ConfigProperties config}.{@link #getChild(String...) getChild}(key1, ..., keyN) is the same as + * {@link ConfigProperties config}.{@link #getChild(String) getChild}(key1)...{@link #getChild(String) getChild}(keyN). + * + * @param keys the keys to traverse recursively. + * @return the descendant {@link #getChild(String) child} reached from recursively traversing the given {@code keys}. + */ + ConfigProperties getChild(String... keys); + + /** + * Shortcut for {@link #getChild(String) getChild(key)}.{@link #getValue()}. + * + * @param key the {@link #getChild(String) key of the child} + * @return the value of this {@link ConfigProperties}-node. May be {@code null}. + */ + String getChildValue(String key); + + /** + * Shortcut for {@link #getChild(String...) getChild(keys)}.{@link #getValue()}. + * + * @param keys the keys to traverse recursively. + * @return the {@link #getValue() value} of the {@link #getChild(String...) descendant child} traversed by + * {@code keys}. May be {@code null}. + */ + String getChildValue(String... keys); + + /** + * @return the value of this {@link ConfigProperties}-node. May be {@code null}. + */ + String getValue(); + + /** + * @param the requested {@code type} + * @param type the {@link Class} reflecting the requested result type. + * @return the value of this {@link ConfigProperties}-node converted to the given {@code type}. Will be {@code null} + * if undefined. + */ + T getValue(Class type); + + /** + * @param the requested {@code type} + * @param type the {@link Class} reflecting the requested result type. + * @param defaultValue the value returned as default if the actual {@link #getValue() value} is undefined. + * @return the value of this {@link ConfigProperties}-node converted to the given {@code type}. Will be + * {@code defaultValue} if undefined. + */ + T getValue(Class type, T defaultValue); + + /** + * @return the {@link #getValue(Class, Object)} as {@code boolean} with {@code false} as default. + */ + boolean getValueAsBoolean(); + + /** + * @return {@code true} if this is an empty {@link ConfigProperties}-node that neither has a {@link #getValue() value} + * nor {@link #getChildKeys() any} {@link #getChild(String) child}. + */ + boolean isEmpty(); + + /** + * @return this {@link ConfigProperties} converted to a {@link ConfigProperties#toFlatMap() flat} {@link Map}. + */ + Map toFlatMap(); + + /** + * @param rootKey the root key used as prefix for the {@link java.util.Map.Entry#getKey() keys} separated with a dot + * if not {@link String#isEmpty() empty}. Typically the empty {@link String}. + * @return this {@link ConfigProperties} converted to a {@link ConfigProperties#toFlatMap() flat} {@link Map}. + */ + Map toFlatMap(String rootKey); + + /** + * @return this {@link ConfigProperties} converted to a {@link ConfigProperties#toHierarchicalMap() hierarchical} + * {@link Map}. + */ + Map toHierarchicalMap(); + + /** + * @param parent the parent {@link ConfigProperties} to extend. + * @return a new instance of {@link ConfigProperties} with all {@link #getChild(String) children} and + * {@link #getValue() value}(s) from this {@link ConfigProperties}-tree and all {@link #getChild(String) + * children} and {@link #getValue() value}(s) inherited from the given {@code parent} + * {@link ConfigProperties}-tree if they are undefined in this tree. + */ + MutableConfigProperties inherit(ConfigProperties parent); + +} diff --git a/modules/basic/src/main/java/com/devonfw/module/basic/common/api/config/ConfigValueUtil.java b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/config/ConfigValueUtil.java new file mode 100644 index 00000000..5431846b --- /dev/null +++ b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/config/ConfigValueUtil.java @@ -0,0 +1,72 @@ +package com.devonfw.module.basic.common.api.config; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Currency; + +/** + * Utility that helps to deal with configuration values. + */ +public final class ConfigValueUtil { + + private ConfigValueUtil() { + } + + /** + * @param the generic {@code type}. + * @param object the {@link Object} to convert. Will not be {@code null}. + * @param type the {@link Class} reflecting the requested type. + * @return the given {@link Object} converted to the given {@code type}. + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static T convertValue(Object object, Class type) { + + try { + Object result; + if (type.isInstance(object)) { + result = object; + } else if (type.isEnum()) { + result = Enum.valueOf((Class) type, object.toString()); + } else if (type.isAssignableFrom(String.class)) { + result = object.toString(); + } else if ((type == boolean.class) || (type == Boolean.class)) { + result = Boolean.valueOf(object.toString()); + } else if ((type == int.class) || (type == Integer.class)) { + result = Integer.valueOf(object.toString()); + } else if ((type == long.class) || (type == Long.class)) { + result = Long.valueOf(object.toString()); + } else if ((type == double.class) || (type == Double.class)) { + result = Double.valueOf(object.toString()); + } else if (type == Class.class) { + result = Class.forName(object.toString()); + } else if ((type == float.class) || (type == Float.class)) { + result = Float.valueOf(object.toString()); + } else if ((type == short.class) || (type == Short.class)) { + result = Short.valueOf(object.toString()); + } else if ((type == byte.class) || (type == Byte.class)) { + result = Byte.valueOf(object.toString()); + } else if (type == BigDecimal.class) { + result = new BigDecimal(object.toString()); + } else if (type == BigInteger.class) { + result = new BigInteger(object.toString()); + } else if (type == Number.class) { + result = Double.parseDouble(object.toString()); + } else if ((type == Character.class) || ((type == char.class))) { + String value = object.toString(); + if (value.length() == 1) { + result = Character.valueOf(value.charAt(0)); + } else { + throw new IllegalArgumentException(value); + } + } else if (type == Currency.class) { + result = Currency.getInstance(object.toString()); + } else { + throw new IllegalArgumentException(object.toString()); + } + return (T) result; + } catch (NumberFormatException | ClassNotFoundException e) { + throw new IllegalArgumentException(object.toString(), e); + } + } + +} diff --git a/modules/basic/src/main/java/com/devonfw/module/basic/common/api/config/EmptyConfigProperties.java b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/config/EmptyConfigProperties.java new file mode 100644 index 00000000..7f3e81fe --- /dev/null +++ b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/config/EmptyConfigProperties.java @@ -0,0 +1,110 @@ +package com.devonfw.module.basic.common.api.config; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +/** + * Implementation of {@link ConfigProperties} that always is {@link ConfigProperties#isEmpty() empty}. + * + * @since 3.0.0 + */ +class EmptyConfigProperties implements ConfigProperties { + + @Override + public Set getChildKeys() { + + return Collections.emptySet(); + } + + @Override + public ConfigProperties getChild(String key) { + + return this; + } + + @Override + public ConfigProperties getChild(String... keys) { + + return this; + } + + @Override + public String getChildValue(String key) { + + return null; + } + + @Override + public String getChildValue(String... keys) { + + return null; + } + + @Override + public String getValue() { + + return null; + } + + @Override + public T getValue(Class type) { + + return null; + } + + @Override + public T getValue(Class type, T defaultValue) { + + return defaultValue; + } + + @Override + public boolean getValueAsBoolean() { + + return false; + } + + @Override + public boolean isEmpty() { + + return true; + } + + @Override + public Map toFlatMap() { + + return toFlatMap(""); + } + + @Override + public Map toFlatMap(String rootKey) { + + return Collections.emptyMap(); + } + + @Override + public Map toHierarchicalMap() { + + return Collections.emptyMap(); + } + + @Override + public MutableConfigProperties inherit(ConfigProperties parent) { + + if (parent == null || parent.isEmpty()) { + return new MutableConfigPropertiesImpl(""); + } + if (parent instanceof MutableConfigProperties) { + return (MutableConfigProperties) parent; + } else + return parent.inherit(EMPTY); + } + + @Override + public String toString() { + + return ""; + } + +} diff --git a/modules/basic/src/main/java/com/devonfw/module/basic/common/api/config/MutableConfigProperties.java b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/config/MutableConfigProperties.java new file mode 100644 index 00000000..6d768f68 --- /dev/null +++ b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/config/MutableConfigProperties.java @@ -0,0 +1,20 @@ +package com.devonfw.module.basic.common.api.config; + +/** + * Extends {@link ConfigProperties} with ability to modify. + */ +public interface MutableConfigProperties extends ConfigProperties { + + /** + * @param key the key of the {@link #getChild(String) child} where to {@link #setValue(String) set the value}. All + * such children will be created if they do not yet exist. + * @param value the new value to {@link #setValue(String) set}. + */ + void setChildValue(String key, String value); + + /** + * @param value the new value of {@link #getValue()}. + */ + void setValue(String value); + +} diff --git a/modules/basic/src/main/java/com/devonfw/module/basic/common/api/config/MutableConfigPropertiesImpl.java b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/config/MutableConfigPropertiesImpl.java new file mode 100644 index 00000000..afcb6d90 --- /dev/null +++ b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/config/MutableConfigPropertiesImpl.java @@ -0,0 +1,120 @@ +package com.devonfw.module.basic.common.api.config; + +/** + * The implementation of {@link MutableConfigProperties}. + */ +class MutableConfigPropertiesImpl extends SimpleConfigProperties implements MutableConfigProperties { + + private final SimpleConfigProperties parent; + + private final SimpleConfigProperties copy; + + private int copyModifications; + + private int parentModifications; + + /** + * The constructor. + * + * @param key the hierarchical key of this {@link ConfigProperties}-node. + */ + protected MutableConfigPropertiesImpl(String key) { + this(key, null, null); + } + + /** + * The constructor. + * + * @param key the hierarchical key of this {@link ConfigProperties}-node. + * @param copy the {@link ConfigProperties}-node to copy with {@link ConfigProperties#inherit(ConfigProperties) + * inheritance} from the given {@code parent}. + * @param parent the parent {@link ConfigProperties} to {@link ConfigProperties#inherit(ConfigProperties) inherit + * from} or {@code null} for none. + */ + protected MutableConfigPropertiesImpl(String key, ConfigProperties copy, ConfigProperties parent) { + super(key); + this.copy = asSimple(copy); + this.parent = asSimple(parent); + this.copyModifications = -1; + this.parentModifications = -1; + } + + @Override + protected void updateChildren() { + + super.updateChildren(); + if (this.copy != null) { + int copyMod = this.copy.getNodeModifications(); + if (this.copyModifications != copyMod) { + for (String key : this.copy.getChildKeys()) { + getChild(key); + } + this.copyModifications = copyMod; + } + } + if (this.parent != null) { + int parentMod = this.parent.getNodeModifications(); + if (this.parentModifications != parentMod) { + for (String key : this.parent.getChildKeys()) { + getChild(key); + } + this.parentModifications = parentMod; + } + } + } + + @Override + public MutableConfigProperties inherit(ConfigProperties parentNode) { + + if ((parentNode == null) || (parentNode.isEmpty())) { + return this; + } + return super.inherit(parentNode); + } + + @Override + public String getValue() { + + String result = super.getValue(); + if (result == null) { + if (this.copy != null) { + result = this.copy.getValue(); + } + if ((result == null) && (this.parent != null)) { + result = this.parent.getValue(); + } + } + return result; + } + + @Override + public void setValue(String value) { + + super.setValue(value); + } + + @Override + public void setChildValue(String key, String value) { + + MutableConfigProperties child = (MutableConfigProperties) getChild(key, true); + child.setValue(value); + } + + @Override + protected SimpleConfigProperties createChild(String key, boolean create) { + + ConfigProperties copyNode = EMPTY; + if (this.copy != null) { + copyNode = this.copy.getChild(key); + } + ConfigProperties parentNode = EMPTY; + if (this.parent != null) { + parentNode = this.parent.getChild(key); + } + if (!create && copyNode.isEmpty() && parentNode.isEmpty()) { + return null; + } + return new MutableConfigPropertiesImpl(key, copyNode, parentNode); + } + +} diff --git a/modules/basic/src/main/java/com/devonfw/module/basic/common/api/config/SimpleConfigProperties.java b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/config/SimpleConfigProperties.java new file mode 100644 index 00000000..28353cc1 --- /dev/null +++ b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/config/SimpleConfigProperties.java @@ -0,0 +1,395 @@ +package com.devonfw.module.basic.common.api.config; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +/** + * Simple implementation of {@link ConfigProperties}. + * + * @since 3.0.0 + */ +public class SimpleConfigProperties implements ConfigProperties { + + private final String nodeKey; + + private final Map childMap; + + private final Set childKeys; + + private String value; + + private int nodeModifications; + + /** + * The constructor. + * + * @param key the hierarchical key of this {@link ConfigProperties}-node. + */ + protected SimpleConfigProperties(String key) { + super(); + this.nodeKey = key; + this.childMap = new HashMap<>(); + this.childKeys = Collections.unmodifiableSet(this.childMap.keySet()); + this.nodeModifications = 0; + } + + @Override + public boolean isEmpty() { + + return false; + } + + /** + * @return the absolute key of this {@link ConfigProperties}-node. + */ + protected String getNodeKey() { + + return this.nodeKey; + } + + /** + * @return the modification counter that gets incremented whenever a child node is added. + */ + protected int getNodeModifications() { + + return this.nodeModifications; + } + + @Override + public Set getChildKeys() { + + updateChildren(); + return this.childKeys; + } + + /** + * Updates the child-nodes in case of a mutable copy node. + */ + protected void updateChildren() { + + // nothing by default + } + + @Override + public ConfigProperties getChild(String key) { + + return getChild(key, false); + } + + /** + * @see #getChild(String) + * + * @param key the key of the requested child. + * @param create - {@code true} to create if not exits, {@code false} otherwise. + * @return the requested child. + */ + protected ConfigProperties getChild(String key, boolean create) { + + if ((key == null) || (key.isEmpty())) { + return this; + } + if (key.indexOf(KEY_SEPARATOR) > 0) { + String[] segments = key.split("\\."); + return getChild(create, segments); + } + SimpleConfigProperties result = this.childMap.get(key); + if (result == null) { + result = createChild(key, create); + if (result == null) { + return EMPTY; + } else { + this.childMap.put(key, result); + this.nodeModifications++; + } + } + return result; + } + + /** + * @param childKey the key segment of the child to create. + * @param create - {@code true} to force creation, {@code false} otherwise. + * @return the new child or {@code null}. + */ + protected SimpleConfigProperties createChild(String childKey, boolean create) { + + if (!create) { + return null; + } + return new SimpleConfigProperties(composeKey(this.nodeKey, childKey)); + } + + @Override + public ConfigProperties getChild(String... keys) { + + return getChild(false, keys); + } + + /** + * @param create - {@code true} to create if not exits, {@code false} otherwise. + * @param keys the key segments of the requested child. + * @return the requested child. + */ + protected ConfigProperties getChild(boolean create, String... keys) { + + if ((keys == null) || (keys.length == 0)) { + return this; + } + SimpleConfigProperties result = this; + for (String key : keys) { + ConfigProperties child = result.getChild(key, create); + if (child.isEmpty()) { + return child; + } + result = (SimpleConfigProperties) child; + } + return result; + } + + @Override + public String getValue() { + + return this.value; + } + + @Override + public T getValue(Class type) { + + return getValue(type, null); + } + + @Override + public T getValue(Class type, T defaultValue) { + + String result = getValue(); + if (result == null) { + return defaultValue; + } else { + return ConfigValueUtil.convertValue(result, type); + } + } + + @Override + public boolean getValueAsBoolean() { + + return "true".equalsIgnoreCase(getValue()); + } + + /** + * @param value new value of {@link #getValue()}. + */ + protected void setValue(String value) { + + this.value = value; + } + + @Override + public String getChildValue(String key) { + + return getChild(key).getValue(); + } + + @Override + public String getChildValue(String... keys) { + + return getChild(keys).getValue(); + } + + @Override + public Map toFlatMap() { + + return toFlatMap(""); + } + + @Override + public Map toFlatMap(String rootKey) { + + Map map = new HashMap<>(); + toFlatMap(rootKey, map); + return map; + } + + private void toFlatMap(String key, Map map) { + + updateChildren(); + String nodeValue = getValue(); + if (nodeValue != null) { + map.put(key, nodeValue); + } + for (Entry entry : this.childMap.entrySet()) { + String childKey = entry.getKey(); + String subKey = composeKey(key, childKey); + entry.getValue().toFlatMap(subKey, map); + } + } + + @Override + public Map toHierarchicalMap() { + + Map map = new HashMap<>(); + toHierarchicalMap(map); + return map; + } + + private void toHierarchicalMap(Map map) { + + String nodeValue = getValue(); + if (nodeValue != null) { + map.put("", nodeValue); + } + updateChildren(); + for (Entry entry : this.childMap.entrySet()) { + String childKey = entry.getKey(); + Map subMap = new HashMap<>(); + entry.getValue().toHierarchicalMap(subMap); + map.put(childKey, subMap); + } + } + + /** + * @see SimpleConfigProperties#ofFlatMap(String, Map) + * + * @param map the flat {@link Map} of the configuration values. + */ + protected void fromFlatMap(Map map) { + + for (Entry entry : map.entrySet()) { + SimpleConfigProperties child; + child = (SimpleConfigProperties) getChild(entry.getKey(), true); + child.value = entry.getValue(); + } + } + + /** + * @see SimpleConfigProperties#ofHierarchicalMap(String, Map) + * + * @param map the hierarchical {@link Map} of the configuration values. + */ + @SuppressWarnings("unchecked") + protected void fromHierarchicalMap(Map map) { + + for (Entry entry : map.entrySet()) { + SimpleConfigProperties child = (SimpleConfigProperties) getChild(entry.getKey(), true); + Object childObject = entry.getValue(); + if (childObject instanceof Map) { + child.fromHierarchicalMap((Map) childObject); + } else { + child.value = childObject.toString(); + } + } + } + + @Override + public String toString() { + + StringBuilder buffer = new StringBuilder(this.nodeKey); + buffer.append('='); + String nodeValue = getValue(); + if (nodeValue != null) { + buffer.append(nodeValue); + } + return buffer.toString(); + } + + @Override + public MutableConfigProperties inherit(ConfigProperties parentNode) { + + return new MutableConfigPropertiesImpl(getNodeKey(), this, parentNode); + } + + /** + * @see #ofFlatMap(String, Map) + * + * @param map the flat {@link Map} of the configuration values. + * @return the root {@link ConfigProperties}-node of the given flat {@link Map} converted to hierarchical + * {@link ConfigProperties}. + */ + public static ConfigProperties ofFlatMap(Map map) { + + return ofFlatMap("", map); + } + + /** + * Converts a flat {@link Map} of configuration values to hierarchical {@link ConfigProperties}. E.g. the flat map + * {"foo.bar.some"="some-value", "foo.bar.other"="other-value"} would result in {@link ConfigProperties} + * {@code myRoot} such that + * myRoot.{@link #getChild(String...) getChild}("foo", "bar").{@link #getChildKeys()} returns the + * {@link Collection} {"some", "other"} and + * myRoot.{@link #getChildValue(String) getValue}("foo.bar.some") returns "my-value". + * + * @param key the top-level key of the returned root {@link ConfigProperties}-node. Typically the empty string ("") + * for root. + * @param map the flat {@link Map} of the configuration values. + * @return the root {@link ConfigProperties}-node of the given flat {@link Map} converted to hierarchical + * {@link ConfigProperties}. + */ + public static ConfigProperties ofFlatMap(String key, Map map) { + + SimpleConfigProperties root = new SimpleConfigProperties(key); + root.fromFlatMap(map); + return root; + } + + /** + * @see #ofHierarchicalMap(String, Map) + * + * @param map the hierarchical {@link Map} of the configuration values. + * @return the root {@link ConfigProperties}-node of the given hierarchical {@link Map} converted to + * {@link ConfigProperties}. + */ + public static ConfigProperties ofHierarchicalMap(Map map) { + + return ofHierarchicalMap("", map); + } + + /** + * Converts a hierarchical {@link Map} of configuration values to {@link ConfigProperties}. E.g. the hierarchical map + * {"foo"={"bar"={"some"="my-value", "other"="magic-value"}}} would result in {@link ConfigProperties} + * {@code myRoot} such that + * myRoot.{@link #getChild(String...) getChild}("foo", "bar").{@link #getChildKeys()} returns the + * {@link Collection} {"some", "other"} and + * myRoot.{@link #getChildValue(String) getValue}("foo.bar.some") returns "my-value". + * + * @param key the top-level key of the returned root {@link ConfigProperties}-node. Typically the empty string ("") + * for root. + * @param map the hierarchical {@link Map} of the configuration values. + * @return the root {@link ConfigProperties}-node of the given hierarchical {@link Map} converted to + * {@link ConfigProperties}. + */ + public static ConfigProperties ofHierarchicalMap(String key, Map map) { + + SimpleConfigProperties root = new SimpleConfigProperties(key); + root.fromHierarchicalMap(map); + return root; + } + + /** + * @param parentKey the parent key. + * @param childKey the child key. + * @return the composed key. + */ + protected static String composeKey(String parentKey, String childKey) { + + if (parentKey.isEmpty()) { + return childKey; + } else { + return parentKey + KEY_SEPARATOR + childKey; + } + } + + /** + * @param configProperties the {@link ConfigProperties}. + * @return the given {@link ConfigProperties} as {@link SimpleConfigProperties} or {@code null} if no such instance. + */ + protected static SimpleConfigProperties asSimple(ConfigProperties configProperties) { + + if (configProperties instanceof SimpleConfigProperties) { + return (SimpleConfigProperties) configProperties; + } + return null; + } + +} diff --git a/modules/basic/src/main/java/com/devonfw/module/basic/common/api/config/SpringProfileConstants.java b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/config/SpringProfileConstants.java new file mode 100644 index 00000000..5b55bd5c --- /dev/null +++ b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/config/SpringProfileConstants.java @@ -0,0 +1,43 @@ +package com.devonfw.module.basic.common.api.config; + +/** + * This class provides {@code String} constants which allow to distinguish several bean definition profiles. The + * constants should be used in {@code @Profile} annotations to avoid multiple points of failure (e.g., through typos + * within annotations).
+ * In test scenarios, these constants should be used in conjunction with the {@code @ActiveProfile} annotation. + * + * @since 2.2.0 + */ +public class SpringProfileConstants { + + /** + * This constant applies to all tests. + */ + public static final String JUNIT = "junit"; + + /** + * This constant denotes a live profile. + */ + public static final String NOT_JUNIT = "!" + JUNIT; + + /** + * This constant should be used in conjunction with component tests. + */ + public static final String COMPONENT_TEST = "component-test"; + + /** + * This constant should be used in conjunction with module tests. + */ + public static final String MODULE_TEST = "module-test"; + + /** + * This constant should be used in conjunction with subsystem tests. + */ + public static final String SUBSYSTEM_TEST = "subsystem-test"; + + /** + * This constant should be used in conjunction with system tests. + */ + public static final String SYSTEM_TEST = "system-test"; + +} diff --git a/modules/basic/src/main/java/com/devonfw/module/basic/common/api/query/LikePatternSyntax.java b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/query/LikePatternSyntax.java new file mode 100644 index 00000000..792c92b3 --- /dev/null +++ b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/query/LikePatternSyntax.java @@ -0,0 +1,140 @@ +package com.devonfw.module.basic.common.api.query; + +/** + * Enum defining available syntaxes for a match pattern in a LIKE-clause. While databases typically require {@link #SQL} + * syntax, human user expect {@link #GLOB} syntax in search forms. Therefore this enum also supports + * {@link #convert(String, LikePatternSyntax, boolean) conversion} from one syntax to another. + * + * @since 3.0.0 + */ +public enum LikePatternSyntax { + + /** + * Glob syntax that is typically expected by end-users and supported by typical search forms. It uses asterisk ('*') + * for {@link #getAny() any wildcard} and question-mark ('?') for {@link #getSingle() single wildcard}. + */ + GLOB('*', '?'), + + /** + * SQL syntax that is typically required by databases. It uses percent ('%') for {@link #getAny() any wildcard} and + * underscore ('_') for {@link #getSingle() single wildcard}. + */ + SQL('%', '_'); + + /** The escape character. */ + public static final char ESCAPE = '\\'; + + private final char any; + + private final char single; + + private LikePatternSyntax(char any, char single) { + + this.any = any; + this.single = single; + } + + /** + * @return the wildcard character that matches any string including the {@link String#isEmpty() empty} string. + */ + public char getAny() { + + return this.any; + } + + /** + * @return the wildcard character that matches exactly one single character. + */ + public char getSingle() { + + return this.single; + } + + /** + * @param pattern the LIKE pattern in the given {@link LikePatternSyntax}. + * @param syntax the {@link LikePatternSyntax} of the given {@code pattern}. + * @return the given {@code pattern} converted to this {@link LikePatternSyntax}. + */ + public String convert(String pattern, LikePatternSyntax syntax) { + + return convert(pattern, syntax, false); + } + + /** + * @param pattern the LIKE pattern in the given {@link LikePatternSyntax}. + * @param syntax the {@link LikePatternSyntax} of the given {@code pattern}. + * @param matchSubstring - {@code true} if the given {@code pattern} shall also match substrings, {@code false} + * otherwise. + * @return the given {@code pattern} converted to this {@link LikePatternSyntax}. + */ + public String convert(String pattern, LikePatternSyntax syntax, boolean matchSubstring) { + + if ((pattern == null) || pattern.isEmpty()) { + if (matchSubstring) { + return Character.toString(this.any); + } else { + return pattern; + } + } + if (this == syntax) { + String result = pattern; + if (matchSubstring) { + if (pattern.charAt(0) != this.any) { + result = this.any + result; + } + int lastIndex = pattern.length() - 1; + if ((pattern.charAt(lastIndex) != this.any) || ((lastIndex > 0) && (pattern.charAt(lastIndex - 1) == ESCAPE))) { + result = result + this.any; + } + } + return result; + } + int length = pattern.length(); + StringBuilder sb = new StringBuilder(length + 8); + boolean lastWildcardAny = false; + for (int i = 0; i < length; i++) { + lastWildcardAny = false; + char c = pattern.charAt(i); + if (c == syntax.any) { + c = this.any; + lastWildcardAny = true; + } else if (c == syntax.single) { + c = this.single; + } else if ((c == this.any) || (c == this.single) || (c == ESCAPE)) { + if ((i == 0) && matchSubstring) { + sb.append(this.any); + } + sb.append(ESCAPE); + } + if (matchSubstring && (i == 0) && !lastWildcardAny) { + sb.append(this.any); + } + sb.append(c); + } + if (matchSubstring && !lastWildcardAny) { + sb.append(this.any); + } + return sb.toString(); + } + + /** + * @param pattern the string value that may be a pattern. + * @return the {@link LikePatternSyntax} for the given {@code pattern} or {@code null} if the given {@code pattern} + * does not contain any wildcards. + */ + public static LikePatternSyntax autoDetect(String pattern) { + + if ((pattern == null) || pattern.isEmpty()) { + return null; + } + for (LikePatternSyntax syntax : values()) { + if (pattern.indexOf(syntax.any) > 0) { + return syntax; + } else if (pattern.indexOf(syntax.single) > 0) { + return syntax; + } + } + return null; + } + +} diff --git a/modules/basic/src/main/java/com/devonfw/module/basic/common/api/query/StringSearchConfigTo.java b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/query/StringSearchConfigTo.java new file mode 100644 index 00000000..d3f43227 --- /dev/null +++ b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/query/StringSearchConfigTo.java @@ -0,0 +1,113 @@ +package com.devonfw.module.basic.common.api.query; + +import com.devonfw.module.basic.common.api.to.AbstractTo; + +/** + * {@link AbstractTo TO} for the options to search for a string value. + * + * @since 3.0.0 + */ +public class StringSearchConfigTo extends AbstractTo { + + private static final long serialVersionUID = 1L; + + private boolean ignoreCase; + + private boolean matchSubstring; + + private LikePatternSyntax likeSyntax; + + private StringSearchOperator operator; + + /** + * @return {@code true} to ignore the case, {@code false} otherwise (to search case-sensitive). + */ + public boolean isIgnoreCase() { + + return this.ignoreCase; + } + + /** + * @param ignoreCase new value of {@link #isIgnoreCase()}. + */ + public void setIgnoreCase(boolean ignoreCase) { + + this.ignoreCase = ignoreCase; + } + + /** + * @return matchSubstring {@code true} if search string shall also match substrings of the string values to search on. + */ + public boolean isMatchSubstring() { + + return this.matchSubstring; + } + + /** + * @param matchSubstring new value of {@link #isMatchSubstring()}. + */ + public void setMatchSubstring(boolean matchSubstring) { + + this.matchSubstring = matchSubstring; + } + + /** + * @return the {@link LikePatternSyntax} of the search string used to do a LIKE-search, {@code null} for no + * LIKE-search. Shall be {@code null} if {@link #getOperator() operator} is neither {@code null} nor + * {@link StringSearchOperator#LIKE}. + */ + public LikePatternSyntax getLikeSyntax() { + + return this.likeSyntax; + } + + /** + * @param likeSyntax new value of {@link #getLikeSyntax()}. + */ + public void setLikeSyntax(LikePatternSyntax likeSyntax) { + + this.likeSyntax = likeSyntax; + } + + /** + * @return operator the {@link StringSearchOperator} used to search. If {@code null} a "magic auto mode" is used where + * {@link StringSearchOperator#LIKE} is used in case the search string contains wildcards and + * {@link StringSearchOperator#EQ} is used otherwise. + */ + public StringSearchOperator getOperator() { + + return this.operator; + } + + /** + * @param operator new value of {@link #getOperator()}. + */ + public void setOperator(StringSearchOperator operator) { + + this.operator = operator; + } + + /** + * @param operator the {@link StringSearchOperator}. + * @return a new {@link StringSearchConfigTo} with the given config. + */ + public static StringSearchConfigTo of(StringSearchOperator operator) { + + StringSearchConfigTo result = new StringSearchConfigTo(); + result.setOperator(operator); + return result; + } + + /** + * @param syntax the {@link LikePatternSyntax}. + * @return a new {@link StringSearchConfigTo} with the given config. + */ + public static StringSearchConfigTo of(LikePatternSyntax syntax) { + + StringSearchConfigTo result = new StringSearchConfigTo(); + result.setOperator(StringSearchOperator.LIKE); + result.setLikeSyntax(syntax); + return result; + } + +} diff --git a/modules/basic/src/main/java/com/devonfw/module/basic/common/api/query/StringSearchOperator.java b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/query/StringSearchOperator.java new file mode 100644 index 00000000..59e237fb --- /dev/null +++ b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/query/StringSearchOperator.java @@ -0,0 +1,63 @@ +package com.devonfw.module.basic.common.api.query; + +/** + * Enum defining available operators for a string search or string comparison. + * + * @since 3.0.0 + */ +public enum StringSearchOperator { + + /** Matches if strings are {@link String#equals(Object) equal}. */ + EQ("=="), + + /** Matches if strings are NOT {@link String#equals(Object) equal}. */ + NE("!="), + + /** Matches if search value is less than search hit(s) in {@link String#compareTo(String) lexicographical order}. */ + LT("<"), + + /** + * Matches if search value is less or equal to search hit(s) in {@link String#compareTo(String) lexicographical + * order}. + */ + LE("<="), + + /** + * Matches if search value is greater than search hit(s) in {@link String#compareTo(String) lexicographical order}. + */ + GT(">"), + + /** + * Matches if search value is greater or equal to search hit(s) in {@link String#compareTo(String) lexicographical + * order}. + */ + GE(">="), + + /** + * Matches if search value as pattern matches search hit(s) in LIKE search. + * + * @see LikePatternSyntax + */ + LIKE("LIKE"), + + /** + * Matches if search value as pattern does not match search hit(s) in LIKE search. + * + * @see LikePatternSyntax + */ + NOT_LIKE("NOT LIKE"); + + private final String operator; + + private StringSearchOperator(String operator) { + + this.operator = operator; + } + + @Override + public String toString() { + + return this.operator; + } + +} diff --git a/modules/basic/src/main/java/com/devonfw/module/basic/common/api/reference/GenericIdRef.java b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/reference/GenericIdRef.java new file mode 100644 index 00000000..d88e2bbd --- /dev/null +++ b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/reference/GenericIdRef.java @@ -0,0 +1,60 @@ +package com.devonfw.module.basic.common.api.reference; + +import java.util.Objects; + +/** + * Generic implementation of {@link Ref}. + * + * @param generic type of {@link #getId() ID}. + * @param generic type of the referenced {@link net.sf.mmm.util.entity.api.Entity}. + */ +public class GenericIdRef implements Ref { + + private static final long serialVersionUID = 1L; + + private final ID id; + + /** + * The constructor. + * + * @param id the {@link #getId() ID}. + */ + public GenericIdRef(ID id) { + + super(); + Objects.requireNonNull(id, "id"); + this.id = id; + } + + @Override + public ID getId() { + + return this.id; + } + + @Override + public int hashCode() { + + return this.id.hashCode(); + } + + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + GenericIdRef other = (GenericIdRef) obj; + return Objects.equals(this.id, other.id); + } + + @Override + public String toString() { + + return this.id.toString(); + } + +} diff --git a/modules/basic/src/main/java/com/devonfw/module/basic/common/api/reference/IdRef.java b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/reference/IdRef.java new file mode 100644 index 00000000..40bc1e65 --- /dev/null +++ b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/reference/IdRef.java @@ -0,0 +1,62 @@ +package com.devonfw.module.basic.common.api.reference; + +import net.sf.mmm.util.entity.api.GenericEntity; + +/** + * A {@link Ref} using {@link Long} values as {@link #getId() ID}. + * + * @param generic type of the referenced {@link net.sf.mmm.util.entity.api.Entity}. + */ +public class IdRef extends GenericIdRef { + + private static final long serialVersionUID = 1L; + + /** + * The constructor. + * + * @param id the {@link #getId() ID}. + */ + public IdRef(Long id) { + + super(id); + } + + /** + * @param generic type of the referenced {@link GenericEntity}. + * @param entity the {@link GenericEntity} to reference. + * @return the {@link IdRef} pointing to the given {@link GenericEntity} or {@code null} if the {@link GenericEntity} + * or its {@link GenericEntity#getId() ID} is {@code null}. + */ + public static > IdRef of(E entity) { + + if (entity == null) { + return null; + } + return of(entity.getId()); + } + + /** + * @param generic type of the referenced {@link GenericEntity}. + * @param id the {@link #getId() ID} to wrap. + * @return the {@link IdRef} pointing to an entity with the specified {@link #getId() ID} or {@code null} if the given + * {@code ID} was {@code null}. + */ + public static IdRef of(Long id) { + + if (id == null) { + return null; + } + return new IdRef<>(id); + } + + /** + * @param generic type of the referenced {@link GenericEntity}. + * @param id the {@link #getId() ID} to wrap. + * @return the {@link IdRef} pointing to an entity with the specified {@link #getId() ID}. + */ + public static IdRef of(long id) { + + return new IdRef<>(Long.valueOf(id)); + } + +} diff --git a/modules/basic/src/main/java/com/devonfw/module/basic/common/api/reference/Ref.java b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/reference/Ref.java new file mode 100644 index 00000000..5d7d2a0a --- /dev/null +++ b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/reference/Ref.java @@ -0,0 +1,22 @@ +package com.devonfw.module.basic.common.api.reference; + +import net.sf.mmm.util.lang.api.Datatype; + +/** + * Interface for a reference to an {@link net.sf.mmm.util.entity.api.GenericEntity entity} via its {@link #getId() ID}. + * In most cases you want to use {@link IdRef}. + * + * @param generic type of {@link #getId() ID}. + * @param generic type of the referenced {@link net.sf.mmm.util.entity.api.Entity}. For flexibility not technically + * bound to {@link net.sf.mmm.util.entity.api.Entity} so it can also be used for an external entity not + * satisfying any requirements. + */ +public interface Ref extends Datatype { + + /** + * @return the ({@link net.sf.mmm.util.entity.api.GenericEntity#getId() ID} of the referenced + * {@link net.sf.mmm.util.entity.api.Entity}. + */ + ID getId(); + +} diff --git a/modules/basic/src/main/java/com/devonfw/module/basic/common/api/reflect/Devon4jPackage.java b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/reflect/Devon4jPackage.java new file mode 100644 index 00000000..0b90fc2c --- /dev/null +++ b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/reflect/Devon4jPackage.java @@ -0,0 +1,452 @@ +package com.devonfw.module.basic.common.api.reflect; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * This class represents a {@link Package} following the + * OASP coding convetion.
+ * After parsing a {@link Package} as {@link Devon4jPackage} you can get individual parts/segments such as + * {@link #getComponent() compoent}, {@link #getLayer() layer}, {@link #getScope() scope}, etc.
+ * This is useful for advanced features and tools such as service clients and exception facades, code-generators, + * static-code-analyzers (SonarQube plugin), etc. + * + * @see #of(String) + * @see #of(Package) + * @see #of(Class) + * + * @author hohwille + */ +public final class Devon4jPackage { + + /** + * The common "layer" for cross-cutting + * code. + */ + public static final String LAYER_COMMON = "common"; + + /** The data-access layer. */ + public static final String LAYER_DATA_ACCESS = "dataaccess"; + + /** The logic layer. */ + public static final String LAYER_LOGIC = "logic"; + + /** The service layer. */ + public static final String LAYER_SERVICE = "service"; + + /** The batch layer. */ + public static final String LAYER_BATCH = "batch"; + + /** + * The client layer. Please note that OASP does + * not recommend to implement the client layer in Java. + */ + public static final String LAYER_CLIENT = "client"; + + /** The scope for APIs. */ + public static final String SCOPE_API = "api"; + + /** + * The scope for reusable base + * implementations. + */ + public static final String SCOPE_BASE = "base"; + + /** The scope for implementations. */ + public static final String SCOPE_IMPL = "impl"; + + private static final Set LAYERS = new HashSet<>( + Arrays.asList(LAYER_BATCH, LAYER_CLIENT, LAYER_COMMON, LAYER_DATA_ACCESS, LAYER_LOGIC, LAYER_SERVICE)); + + private static final Set SCOPES = new HashSet<>(Arrays.asList(SCOPE_API, SCOPE_BASE, SCOPE_IMPL)); + + private static final String REGEX_PKG_SEPARATOR = "\\."; + + private final String[] segments; + + private final int scopeIndex; + + private Boolean valid; + + private transient String root; + + private transient String detail; + + private transient String pkg; + + /** + * Der Konstruktor. + * + * @param segments - see {@link #getSegment(int)}. + * @param scope - see {@link #getScope()}. + */ + private Devon4jPackage(String pkg, String[] segments, String root, String detail, int scope) { + super(); + Objects.requireNonNull(segments, "segments"); + this.pkg = pkg; + for (int i = 0; i < segments.length; i++) { + if (!isValidSegment(segments[i])) { + throw new IllegalArgumentException("segments[" + i + "] = " + segments[i]); + } + } + this.root = root; + this.detail = detail; + this.segments = segments; + this.scopeIndex = scope; + } + + private static boolean isValidSegment(String segment) { + + if (segment == null) { + return false; + } + if (segment.isEmpty()) { + return false; + } + if (segment.indexOf('.') >= 0) { + return false; + } + return true; + } + + /** + * @return the number of {@link #getSegment(int) package segments}. + */ + public int getSegmentCount() { + + return this.segments.length; + } + + /** + * @param index the position of the requested segment. A valid index is in the range from {@code 0} to + * {@link #getSegmentCount()}-1. + * @return the {@link Package} segment at the given index or {@code null} if the given index is invalid. + */ + public String getSegment(int index) { + + if ((index >= 0) && (index < this.segments.length)) { + return this.segments[index]; + } + return null; + } + + /** + * @return {@code true} if this {@link Devon4jPackage} is a valid according to OASP + * package conventions", + * {@code false} otherwise. + */ + public boolean isValid() { + + if (this.valid == null) { + this.valid = Boolean.valueOf(isValidInternal()); + } + return this.valid; + } + + private boolean isValidInternal() { + + if (this.segments.length < 4) { + return false; + } + if (!isValidLayer()) { + return false; + } + if (!isValidScope()) { + return false; + } + return true; + } + + /** + * @return {@code true} if the {@link #getScope() scope} is valid (one of the predefined scopes {@link #isScopeApi() + * api}, {@link #isScopeBase() base}, or {@link #isScopeImpl() impl}). + */ + public boolean isValidScope() { + + return SCOPES.contains(getScope()); + } + + /** + * @return {@code true} if the {@link #getLayer() layer} is valid (one of the predefined scopes {@link #isLayerBatch() + * batch}, {@link #isLayerClient() client}, {@link #isLayerCommon() common}, {@link #isLayerDataAccess() + * dataaccess}, {@link #isLayerLogic() logic}, or {@link #isLayerService() service}). + */ + public boolean isValidLayer() { + + return LAYERS.contains(getLayer()); + } + + /** + * @return the root-{@link Package} of the organization or IT project owning the code. + */ + public String getRoot() { + + if (this.root == null) { + if (this.scopeIndex == -1) { + return this.pkg; + } + int appIndex = this.scopeIndex - 3; + if (appIndex <= 0) { + return null; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < appIndex; i++) { + if (i > 0) { + sb.append('.'); + } + sb.append(this.segments[i]); + } + this.root = sb.toString(); + } + return this.root; + } + + /** + * @return the technical name of the application or (micro-)service. + */ + public String getApplication() { + + return getSegment(this.scopeIndex - 3); + } + + /** + * @return the business component the code belongs to. + */ + public String getComponent() { + + return getSegment(this.scopeIndex - 2); + } + + /** + * @return the layer the code is assigned to. + */ + public String getLayer() { + + return getSegment(this.scopeIndex - 1); + } + + /** + * @return {@code true} if {@link #getLayer() layer} is {@link #LAYER_COMMON}. + */ + public boolean isLayerCommon() { + + return LAYER_COMMON.equals(getLayer()); + } + + /** + * @return {@code true} if {@link #getLayer() layer} is {@link #LAYER_DATA_ACCESS}. + */ + public boolean isLayerDataAccess() { + + return LAYER_DATA_ACCESS.equals(getLayer()); + } + + /** + * @return {@code true} if {@link #getLayer() layer} is {@link #LAYER_LOGIC}. + */ + public boolean isLayerLogic() { + + return LAYER_LOGIC.equals(getLayer()); + } + + /** + * @return {@code true} if {@link #getLayer() layer} is {@link #LAYER_SERVICE}. + */ + public boolean isLayerService() { + + return LAYER_SERVICE.equals(getLayer()); + } + + /** + * @return {@code true} if {@link #getLayer() layer} is {@link #LAYER_BATCH}. + */ + public boolean isLayerBatch() { + + return LAYER_BATCH.equals(getLayer()); + } + + /** + * @return {@code true} if {@link #getLayer() layer} is {@link #LAYER_CLIENT}. + */ + public boolean isLayerClient() { + + return LAYER_CLIENT.equals(getLayer()); + } + + /** + * @return scope the scope the code is assigned to. + */ + public String getScope() { + + return getSegment(this.scopeIndex); + } + + /** + * @return {@code true} if {@link #getScope() scope} is {@link #SCOPE_API}. + */ + public boolean isScopeApi() { + + return SCOPE_API.equals(getScope()); + } + + /** + * @return {@code true} if {@link #getScope() scope} is {@link #SCOPE_BASE}. + */ + public boolean isScopeBase() { + + return SCOPE_BASE.equals(getScope()); + } + + /** + * @return {@code true} if {@link #getScope() scope} is {@link #SCOPE_IMPL}. + */ + public boolean isScopeImpl() { + + return SCOPE_IMPL.equals(getScope()); + } + + /** + * @return the optional detail. Can be a single segment or multiple segments separated with dot. May be {@code null}. + */ + public String getDetail() { + + if (this.detail == null) { + if (this.scopeIndex < 3) { + return null; + } + this.detail = joinPackage(this.scopeIndex + 1); + } + return this.detail; + } + + @Override + public int hashCode() { + + return Objects.hash(this.segments, this.scopeIndex); + } + + @Override + public boolean equals(Object obj) { + + if (obj == this) { + return true; + } + if ((obj == null) || (obj.getClass() != Devon4jPackage.class)) { + return false; + } + Devon4jPackage other = (Devon4jPackage) obj; + if (!Arrays.deepEquals(this.segments, other.segments)) { + return false; + } + if (this.scopeIndex != other.scopeIndex) { + return false; + } + return true; + } + + @Override + public String toString() { + + if (this.pkg == null) { + this.pkg = joinPackage(0); + } + return this.pkg; + } + + private String joinPackage(int start) { + + return joinPackage(start, this.segments.length); + } + + private String joinPackage(int start, int end) { + + if (start >= end) { + return null; + } + StringBuilder sb = new StringBuilder(); + for (int i = start; i < end; i++) { + if (i > start) { + sb.append('.'); + } + sb.append(this.segments[i]); + } + return sb.toString(); + } + + /** + * @param root - see {@link #getRoot()}. + * @param application - see {@link #getApplication()}. + * @param component - see {@link #getComponent()}. + * @param layer - see {@link #getLayer()}. + * @param scope - see {@link #getScope()}. + * @param detail - see {@link #getDetail()}. + * @return the {@link Devon4jPackage} for the given parameters. + */ + public static Devon4jPackage of(String root, String application, String component, String layer, String scope, + String detail) { + + String[] roots; + if (root == null) { + roots = new String[0]; + } else { + roots = root.split(REGEX_PKG_SEPARATOR); + } + String[] details; + if (detail == null) { + details = new String[0]; + } else { + details = detail.split(REGEX_PKG_SEPARATOR); + } + String[] segments = new String[roots.length + details.length + 4]; + System.arraycopy(roots, 0, segments, 0, roots.length); + int i = roots.length; + segments[i++] = application; + segments[i++] = component; + segments[i++] = layer; + segments[i++] = scope; + System.arraycopy(details, 0, segments, i, details.length); + return new Devon4jPackage(null, segments, root, detail, (i - 1)); + } + + /** + * @param packageName the {@link Package#getName() package name} to parse. + * @return the parsed {@link Devon4jPackage} corresponding to the given package. + */ + public static Devon4jPackage of(String packageName) { + + String[] segments = packageName.split(REGEX_PKG_SEPARATOR); + int scopeIndex = -1; + for (int i = 2; i < segments.length; i++) { + if (SCOPES.contains(segments[i])) { + scopeIndex = i; + break; + } + if (LAYERS.contains(segments[i])) { + scopeIndex = i + 1; + } + } + return new Devon4jPackage(packageName, segments, null, null, scopeIndex); + } + + /** + * @param javaPackage the {@link Package} to parse. + * @return the parsed {@link Devon4jPackage} corresponding to the given package. + */ + public static Devon4jPackage of(Package javaPackage) { + + return of(javaPackage.getName()); + } + + /** + * @param type the {@link Class} {@link Class#getPackage() located} in the {@link Package} to parse. + * @return the parsed {@link Devon4jPackage} corresponding to the {@link Package} {@link Class#getPackage() of} the given + * {@link Class}. + */ + public static Devon4jPackage of(Class type) { + + return of(type.getPackage()); + } + +} diff --git a/modules/basic/src/main/java/com/devonfw/module/basic/common/api/to/AbstractCto.java b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/to/AbstractCto.java new file mode 100644 index 00000000..ec9a20c4 --- /dev/null +++ b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/to/AbstractCto.java @@ -0,0 +1,26 @@ +package com.devonfw.module.basic.common.api.to; + +import net.sf.mmm.util.transferobject.api.AbstractTransferObject; +import net.sf.mmm.util.transferobject.api.TransferObject; + +/** + * This is the abstract base class for a composite {@link AbstractTo transfer-object}. Such object should contain + * (aggregate) other {@link AbstractTransferObject}s but no atomic data. This means it has properties that contain a + * {@link TransferObject} or a {@link java.util.Collection} of those but no {@link net.sf.mmm.util.lang.api.Datatype + * values}.
+ * Classes extending this class should carry the suffix Cto. + * + */ +public abstract class AbstractCto extends AbstractTo { + + private static final long serialVersionUID = 1L; + + /** + * The constructor. + */ + public AbstractCto() { + + super(); + } + +} diff --git a/modules/basic/src/main/java/com/devonfw/module/basic/common/api/to/AbstractEto.java b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/to/AbstractEto.java new file mode 100644 index 00000000..b60d5f63 --- /dev/null +++ b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/to/AbstractEto.java @@ -0,0 +1,146 @@ +package com.devonfw.module.basic.common.api.to; + +import net.sf.mmm.util.entity.api.GenericEntity; +import net.sf.mmm.util.entity.api.MutableRevisionedEntity; +import net.sf.mmm.util.entity.api.PersistenceEntity; +import net.sf.mmm.util.transferobject.api.TransferObject; + +/** + * This is the abstract base class for an {@link TransferObject} that only contains data without relations. This is + * called DTO (data transfer object). Here data means properties that typically represent a + * {@link net.sf.mmm.util.lang.api.Datatype} and potentially for relations the ID (as {@link Long}). For actual + * relations you will use {@link AbstractCto CTO}s to express what set of entities to transfer, load, save, update, etc. + * without redundancies. It typically corresponds to an {@link net.sf.mmm.util.entity.api.GenericEntity entity}. For + * additional details and an example consult the. + * + */ +public abstract class AbstractEto extends AbstractTo implements MutableRevisionedEntity { + + private static final long serialVersionUID = 1L; + + /** @see #getId() */ + private Long id; + + /** @see #getModificationCounter() */ + private int modificationCounter; + + /** @see #getRevision() */ + private Number revision; + + /** + * @see #getModificationCounter() + */ + private transient GenericEntity persistentEntity; + + /** + * The constructor. + */ + public AbstractEto() { + + super(); + this.revision = LATEST_REVISION; + } + + /** + * {@inheritDoc} + */ + @Override + public Long getId() { + + return this.id; + } + + /** + * {@inheritDoc} + */ + @Override + public void setId(Long id) { + + this.id = id; + } + + /** + * {@inheritDoc} + */ + @Override + public int getModificationCounter() { + + if (this.persistentEntity != null) { + // JPA implementations will update modification counter only after the transaction has been committed. + // Conversion will typically happen before and would result in the wrong (old) modification counter. + // Therefore we update the modification counter here (that has to be called before serialization takes + // place). + this.modificationCounter = this.persistentEntity.getModificationCounter(); + } + return this.modificationCounter; + } + + /** + * {@inheritDoc} + */ + @Override + public void setModificationCounter(int version) { + + this.modificationCounter = version; + } + + /** + * {@inheritDoc} + */ + @Override + public Number getRevision() { + + return this.revision; + } + + /** + * {@inheritDoc} + */ + @Override + public void setRevision(Number revision) { + + this.revision = revision; + } + + /** + * Method to extend {@link #toString()} logic. + * + * @param buffer is the {@link StringBuilder} where to {@link StringBuilder#append(Object) append} the string + * representation. + */ + @Override + protected void toString(StringBuilder buffer) { + + super.toString(buffer); + if (this.id != null) { + buffer.append("[id="); + buffer.append(this.id); + buffer.append("]"); + } + if (this.revision != null) { + buffer.append("[rev="); + buffer.append(this.revision); + buffer.append("]"); + } + } + + /** + * Inner class to grant access to internal {@link PersistenceEntity} reference of an {@link AbstractEto}. Shall only + * be used internally and never be external users. + */ + public static class PersistentEntityAccess { + + /** + * Sets the internal {@link PersistenceEntity} reference of the given {@link AbstractEto}. + * + * @param is the generic type of the {@link GenericEntity#getId() ID}. + * @param eto is the {@link AbstractEto}. + * @param persistentEntity is the {@link PersistenceEntity}. + */ + protected void setPersistentEntity(AbstractEto eto, PersistenceEntity persistentEntity) { + + assert ((eto.persistentEntity == null) || (persistentEntity == null)); + eto.persistentEntity = persistentEntity; + } + } +} diff --git a/modules/basic/src/main/java/com/devonfw/module/basic/common/api/to/AbstractTo.java b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/to/AbstractTo.java new file mode 100644 index 00000000..272780d4 --- /dev/null +++ b/modules/basic/src/main/java/com/devonfw/module/basic/common/api/to/AbstractTo.java @@ -0,0 +1,46 @@ +package com.devonfw.module.basic.common.api.to; + +import net.sf.mmm.util.transferobject.api.TransferObject; + +/** + * Abstract class for a plain {@link net.sf.mmm.util.transferobject.api.TransferObject} that is neither a + * {@link AbstractEto ETO} nor a {@link AbstractCto CTO}. Classes extending this class should carry the suffix + * Cto.
+ * + */ +public abstract class AbstractTo implements TransferObject { + + /** UID for serialization. */ + private static final long serialVersionUID = 1L; + + /** + * The constructor. + */ + public AbstractTo() { + + super(); + } + + /** + * {@inheritDoc} + */ + @Override + public final String toString() { + + StringBuilder buffer = new StringBuilder(); + toString(buffer); + return buffer.toString(); + } + + /** + * Method to extend {@link #toString()} logic. Override to add additional information. + * + * @param buffer is the {@link StringBuilder} where to {@link StringBuilder#append(Object) append} the string + * representation. + */ + protected void toString(StringBuilder buffer) { + + buffer.append(getClass().getSimpleName()); + } + +} diff --git a/modules/basic/src/main/java/com/devonfw/module/basic/configuration/SpringProfileConstants.java b/modules/basic/src/main/java/com/devonfw/module/basic/configuration/SpringProfileConstants.java new file mode 100644 index 00000000..e096eadc --- /dev/null +++ b/modules/basic/src/main/java/com/devonfw/module/basic/configuration/SpringProfileConstants.java @@ -0,0 +1,45 @@ +package com.devonfw.module.basic.configuration; + +/** + * This class provides {@code String} constants which allow to distinguish several bean definition profiles. The + * constants should be used in {@code @Profile} annotations to avoid multiple points of failure (e.g., through typos + * within annotations).
+ * In test scenarios, these constants should be used in conjunction with the {@code @ActiveProfile} annotation. + * + * @since 2.1.0 + * @deprecated please use {@link com.devonfw.module.basic.common.api.config.SpringProfileConstants} instead. + */ +@Deprecated +public class SpringProfileConstants { + + /** + * This constant applies to all tests. + */ + public static final String JUNIT = "junit"; + + /** + * This constant denotes a live profile. + */ + public static final String NOT_JUNIT = "!" + JUNIT; + + /** + * This constant should be used in conjunction with component tests. + */ + public static final String COMPONENT_TEST = "component-test"; + + /** + * This constant should be used in conjunction with module tests. + */ + public static final String MODULE_TEST = "module-test"; + + /** + * This constant should be used in conjunction with subsystem tests. + */ + public static final String SUBSYSTEM_TEST = "subsystem-test"; + + /** + * This constant should be used in conjunction with system tests. + */ + public static final String SYSTEM_TEST = "system-test"; + +} diff --git a/modules/basic/src/test/java/com/devonfw/module/basic/common/api/config/SimplConfigPropertiesTest.java b/modules/basic/src/test/java/com/devonfw/module/basic/common/api/config/SimplConfigPropertiesTest.java new file mode 100644 index 00000000..983e5456 --- /dev/null +++ b/modules/basic/src/test/java/com/devonfw/module/basic/common/api/config/SimplConfigPropertiesTest.java @@ -0,0 +1,136 @@ +package com.devonfw.module.basic.common.api.config; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.assertj.core.data.MapEntry; +import org.junit.Test; + +import com.devonfw.module.basic.common.api.config.ConfigProperties; +import com.devonfw.module.basic.common.api.config.EmptyConfigProperties; +import com.devonfw.module.basic.common.api.config.SimpleConfigProperties; +import com.devonfw.module.test.common.base.ModuleTest; + +/** + * Test of {@link SimpleConfigProperties}. + */ +public class SimplConfigPropertiesTest extends ModuleTest { + + /** + * Test of {@link SimpleConfigProperties#ofFlatMap(String, Map)}. + */ + @Test + public void testOfFlatMap() { + + // given + Map map = new HashMap<>(); + map.put("foo.bar.some", "some-value"); + map.put("foo.bar.other", "other-value"); + map.put("foo", "foo-value"); + map.put("bar", "bar-value"); + map.put("", "value"); + + // when + ConfigProperties rootConfig = SimpleConfigProperties.ofFlatMap("root", map); + + // then + assertThat(rootConfig).isNotNull(); + assertThat(rootConfig.isEmpty()).isFalse(); + assertThat(rootConfig.getValue()).isEqualTo("value"); + assertThat(rootConfig.toString()).isEqualTo("root=value"); + assertThat(rootConfig.getChildKeys()).containsOnly("foo", "bar"); + assertThat(rootConfig.getChild("foo", "bar").getChildKeys()).containsOnly("some", "other"); + assertThat(rootConfig.getChild("undefined")).isNotNull().isInstanceOf(EmptyConfigProperties.class); + for (Entry entry : map.entrySet()) { + String key = entry.getKey(); + assertThat(rootConfig.getChildValue(key)).as(key).isEqualTo(entry.getValue()); + } + assertThat(rootConfig.toFlatMap()).isEqualTo(map); + } + + /** + * Test of {@link SimpleConfigProperties#ofHierarchicalMap(String, Map)}. + */ + @Test + public void testOfHierarchicalMap() { + + // given + Map fooMap = createMap("foo-value"); + Map fooBarMap = new HashMap<>(); + fooMap.put("bar", fooBarMap); + fooBarMap.put("some", createMap("some-value")); + fooBarMap.put("other", createMap("other-value")); + Map barMap = createMap("bar-value"); + + Map map = createMap("value"); + map.put("foo", fooMap); + map.put("bar", barMap); + + // when + ConfigProperties rootConfig = SimpleConfigProperties.ofHierarchicalMap("root", map); + + // then + assertThat(rootConfig).isNotNull(); + assertThat(rootConfig.isEmpty()).isFalse(); + assertThat(rootConfig.getValue()).isEqualTo("value"); + assertThat(rootConfig.toString()).isEqualTo("root=value"); + assertThat(rootConfig.getChildKeys()).containsOnly("foo", "bar"); + assertThat(rootConfig.getChild("foo", "bar").getChildKeys()).containsOnly("some", "other"); + assertThat(rootConfig.getChild("undefined")).isNotNull().isInstanceOf(EmptyConfigProperties.class); + assertThat(rootConfig.toHierarchicalMap()).isEqualTo(map); + for (Entry entry : rootConfig.toFlatMap().entrySet()) { + String key = entry.getKey(); + assertThat(rootConfig.getChildValue(key)).as(key).isEqualTo(entry.getValue()); + } + } + + /** + * Test of {@link SimpleConfigProperties#inherit(ConfigProperties)}. + */ + @Test + public void testInherit() { + + // given + Map map = new HashMap<>(); + map.put("app.foo.url", "http://foo.domain.com"); + map.put("app.bar.url", "http://bar.domain.com"); + map.put("app.bar.user.login", "bar-user"); + map.put("default.url", "http://api.domain.com"); + map.put("default.user.login", "api"); + + // when + ConfigProperties rootConfig = SimpleConfigProperties.ofFlatMap(map); + ConfigProperties defaultConfig = rootConfig.getChild("default"); + ConfigProperties fooAppConfig = rootConfig.getChild("app", "foo"); + ConfigProperties fooConfig = fooAppConfig.inherit(defaultConfig); + ConfigProperties barAppConfig = rootConfig.getChild("app", "bar"); + ConfigProperties barConfig = barAppConfig.inherit(defaultConfig); + + // then + assertThat(fooConfig).isNotNull(); + assertThat(fooConfig.toFlatMap()).containsOnly(MapEntry.entry("url", "http://foo.domain.com"), + MapEntry.entry("user.login", "api")); + fooConfig = fooAppConfig.inherit(defaultConfig); // test lazy init + assertThat(fooConfig.getChildKeys()).containsOnly("url", "user"); + fooConfig = fooAppConfig.inherit(defaultConfig); // test lazy init + assertThat(fooConfig.getChildValue("url")).isEqualTo("http://foo.domain.com"); + assertThat(fooConfig.getChildValue("user", "login")).isEqualTo("api"); + assertThat(barConfig).isNotNull(); + assertThat(barConfig.toFlatMap()).containsOnly(MapEntry.entry("url", "http://bar.domain.com"), + MapEntry.entry("user.login", "bar-user")); + barConfig = barAppConfig.inherit(defaultConfig); // test lazy init + assertThat(barConfig.getChildKeys()).containsOnly("url", "user"); + barConfig = barAppConfig.inherit(defaultConfig); // test lazy init + assertThat(barConfig.getChildValue("url")).isEqualTo("http://bar.domain.com"); + assertThat(barConfig.getChildValue("user", "login")).isEqualTo("bar-user"); + } + + private static Map createMap(String value) { + + Map map = new HashMap<>(); + map.put("", value); + return map; + } + +} diff --git a/modules/basic/src/test/java/com/devonfw/module/basic/common/api/query/LikePatternSyntaxTest.java b/modules/basic/src/test/java/com/devonfw/module/basic/common/api/query/LikePatternSyntaxTest.java new file mode 100644 index 00000000..3805cb1a --- /dev/null +++ b/modules/basic/src/test/java/com/devonfw/module/basic/common/api/query/LikePatternSyntaxTest.java @@ -0,0 +1,83 @@ +package com.devonfw.module.basic.common.api.query; + +import org.junit.Test; + +import com.devonfw.module.basic.common.api.query.LikePatternSyntax; +import com.devonfw.module.test.common.base.ModuleTest; + +/** + * Test of {@link LikePatternSyntax}. + */ +public class LikePatternSyntaxTest extends ModuleTest { + + /** Basic test of {@link LikePatternSyntax#GLOB}. */ + @Test + public void testGlob() { + + LikePatternSyntax syntax = LikePatternSyntax.GLOB; + assertThat(syntax.getAny()).isEqualTo('*'); + assertThat(syntax.getSingle()).isEqualTo('?'); + } + + /** Basic test of {@link LikePatternSyntax#SQL}. */ + @Test + public void testSql() { + + LikePatternSyntax syntax = LikePatternSyntax.SQL; + assertThat(syntax.getAny()).isEqualTo('%'); + assertThat(syntax.getSingle()).isEqualTo('_'); + } + + /** + * Test of {@link LikePatternSyntax#convert(String, LikePatternSyntax, boolean)} from {@link LikePatternSyntax#GLOB} + * to {@link LikePatternSyntax#SQL}. + */ + @Test + public void testGlob2Sql() { + + LikePatternSyntax syntax = LikePatternSyntax.GLOB; + assertThat(LikePatternSyntax.SQL.convert("", syntax)).isEqualTo(""); + assertThat(LikePatternSyntax.SQL.convert("*", syntax)).isEqualTo("%"); + assertThat(LikePatternSyntax.SQL.convert("?", syntax)).isEqualTo("_"); + assertThat(LikePatternSyntax.SQL.convert("a*b?c", syntax)).isEqualTo("a%b_c"); + assertThat(LikePatternSyntax.SQL.convert("*10% key_loss*", syntax)).isEqualTo("%10\\% key\\_loss%"); + + assertThat(LikePatternSyntax.SQL.convert("a", syntax, true)).isEqualTo("%a%"); + assertThat(LikePatternSyntax.SQL.convert("*", syntax, true)).isEqualTo("%"); + assertThat(LikePatternSyntax.SQL.convert("a*b?c", syntax, true)).isEqualTo("%a%b_c%"); + } + + /** + * Test of {@link LikePatternSyntax#convert(String, LikePatternSyntax, boolean)} from {@link LikePatternSyntax#SQL} to + * {@link LikePatternSyntax#GLOB}. + */ + @Test + public void testSql2Glob() { + + LikePatternSyntax syntax = LikePatternSyntax.SQL; + assertThat(LikePatternSyntax.GLOB.convert("", syntax)).isEqualTo(""); + assertThat(LikePatternSyntax.GLOB.convert("%", syntax)).isEqualTo("*"); + assertThat(LikePatternSyntax.GLOB.convert("_", syntax)).isEqualTo("?"); + assertThat(LikePatternSyntax.GLOB.convert("a%b_c", syntax)).isEqualTo("a*b?c"); + assertThat(LikePatternSyntax.GLOB.convert("%10* key?loss%", syntax)).isEqualTo("*10\\* key\\?loss*"); + + assertThat(LikePatternSyntax.GLOB.convert("a", syntax, true)).isEqualTo("*a*"); + assertThat(LikePatternSyntax.GLOB.convert("%", syntax, true)).isEqualTo("*"); + assertThat(LikePatternSyntax.GLOB.convert("a%b_c", syntax, true)).isEqualTo("*a*b?c*"); + } + + /** Test of {@link LikePatternSyntax#autoDetect(String)}. */ + @Test + public void testAutoDetect() { + + assertThat(LikePatternSyntax.autoDetect(null)).isEqualTo(null); + assertThat(LikePatternSyntax.autoDetect("")).isEqualTo(null); + assertThat(LikePatternSyntax.autoDetect("a")).isEqualTo(null); + assertThat(LikePatternSyntax.autoDetect("aBc")).isEqualTo(null); + assertThat(LikePatternSyntax.autoDetect("a*b")).isEqualTo(LikePatternSyntax.GLOB); + assertThat(LikePatternSyntax.autoDetect("a?b")).isEqualTo(LikePatternSyntax.GLOB); + assertThat(LikePatternSyntax.autoDetect("a%b")).isEqualTo(LikePatternSyntax.SQL); + assertThat(LikePatternSyntax.autoDetect("a_b")).isEqualTo(LikePatternSyntax.SQL); + } + +} diff --git a/modules/basic/src/test/java/com/devonfw/module/basic/common/api/reference/IdRefTest.java b/modules/basic/src/test/java/com/devonfw/module/basic/common/api/reference/IdRefTest.java new file mode 100644 index 00000000..ab5c3a02 --- /dev/null +++ b/modules/basic/src/test/java/com/devonfw/module/basic/common/api/reference/IdRefTest.java @@ -0,0 +1,80 @@ +package com.devonfw.module.basic.common.api.reference; + +import net.sf.mmm.util.entity.api.MutableGenericEntity; + +import org.junit.Test; + +import com.devonfw.module.basic.common.api.reference.IdRef; +import com.devonfw.module.basic.common.api.to.AbstractEto; +import com.devonfw.module.test.common.base.ModuleTest; + +/** + * Test of {@link IdRef}. + */ +public class IdRefTest extends ModuleTest { + + /** Test of {@link IdRef#of(net.sf.mmm.util.entity.api.GenericEntity)} */ + @Test + public void testOfEntity() { + + // given + long id = 4711L; + FooEto foo = new FooEto(); + foo.setId(id); + + // when + IdRef fooId = IdRef. of(foo); // with Java8 type-inference the additional is not required + + // then + assertThat(fooId).isNotNull(); + assertThat(fooId.getId()).isEqualTo(foo.getId()).isEqualTo(id); + assertThat(fooId.toString()).isEqualTo(Long.toString(id)); + assertThat(IdRef.of((Foo) null)).isNull(); + + Bar bar = new Bar(); + bar.setFooId(fooId); // just a syntax/compilation check. + // bar.setBarId(fooId); // will produce compiler error what is desired in such case + IdRef barId = IdRef.of(1234L); + bar.setBarId(barId); // this again will compile + } + + /** Test of {@link IdRef#of(Long)} */ + @Test + public void testOfLong() { + + // given + long id = 4711L; + + // when + IdRef fooId = IdRef.of(id); // not type-safe but required in some cases + + // then + assertThat(fooId).isNotNull(); + assertThat(fooId.getId()).isEqualTo(id); + assertThat(fooId.toString()).isEqualTo(Long.toString(id)); + assertThat(IdRef.of(Long.valueOf(id))).isEqualTo(fooId).isNotSameAs(fooId); + assertThat(IdRef.of((Long) null)).isNull(); + } + + private interface Foo extends MutableGenericEntity { + + } + + private class FooEto extends AbstractEto implements Foo { + + private static final long serialVersionUID = 1L; + + } + + private class Bar { + + void setFooId(IdRef fooId) { + + } + + void setBarId(IdRef fooId) { + + } + } + +} diff --git a/modules/basic/src/test/java/com/devonfw/module/basic/common/api/reflect/Devon4jPackageTest.java b/modules/basic/src/test/java/com/devonfw/module/basic/common/api/reflect/Devon4jPackageTest.java new file mode 100644 index 00000000..987c00b6 --- /dev/null +++ b/modules/basic/src/test/java/com/devonfw/module/basic/common/api/reflect/Devon4jPackageTest.java @@ -0,0 +1,194 @@ +package com.devonfw.module.basic.common.api.reflect; + +import org.junit.Test; + +import com.devonfw.module.test.common.base.ModuleTest; + +/** + * Test-case for {@link Devon4jPackage}. + * + * @author hohwille + */ +public class Devon4jPackageTest extends ModuleTest { + + /** Test of {@link Devon4jPackage#of(Class)} with {@link Devon4jPackage}. */ + @Test + public void testOfClass() { + + Class type = Devon4jPackage.class; + Devon4jPackage pkg = Devon4jPackage.of(type); + assertThat(pkg.getRoot()).isEqualTo("com.devonfw"); + assertThat(pkg.getApplication()).isEqualTo("module"); + assertThat(pkg.getComponent()).isEqualTo("basic"); + assertThat(pkg.getLayer()).isEqualTo("common"); + assertThat(pkg.isLayerBatch()).isFalse(); + assertThat(pkg.isLayerClient()).isFalse(); + assertThat(pkg.isLayerCommon()).isTrue(); + assertThat(pkg.isLayerDataAccess()).isFalse(); + assertThat(pkg.isLayerLogic()).isFalse(); + assertThat(pkg.isLayerService()).isFalse(); + assertThat(pkg.getScope()).isEqualTo("api"); + assertThat(pkg.isScopeApi()).isTrue(); + assertThat(pkg.isScopeBase()).isFalse(); + assertThat(pkg.isScopeImpl()).isFalse(); + assertThat(pkg.getDetail()).isEqualTo("reflect"); + assertThat(pkg.toString()).isEqualTo(type.getPackage().getName()); + assertThat(pkg.isValid()).isTrue(); + } + + /** Test of {@link Devon4jPackage#of(String)} with {@code com.devonfw.module.rest.service.impl.json}. */ + @Test + public void testOfStringDevon4jModule() { + + String packageName = "com.devonfw.module.rest.service.impl.json"; + Devon4jPackage pkg = Devon4jPackage.of(packageName); + assertThat(pkg.getRoot()).isEqualTo("com.devonfw"); + assertThat(pkg.getApplication()).isEqualTo("module"); + assertThat(pkg.getComponent()).isEqualTo("rest"); + assertThat(pkg.getLayer()).isEqualTo("service"); + assertThat(pkg.isLayerBatch()).isFalse(); + assertThat(pkg.isLayerClient()).isFalse(); + assertThat(pkg.isLayerCommon()).isFalse(); + assertThat(pkg.isLayerDataAccess()).isFalse(); + assertThat(pkg.isLayerLogic()).isFalse(); + assertThat(pkg.isLayerService()).isTrue(); + assertThat(pkg.getScope()).isEqualTo("impl"); + assertThat(pkg.isScopeApi()).isFalse(); + assertThat(pkg.isScopeBase()).isFalse(); + assertThat(pkg.isScopeImpl()).isTrue(); + assertThat(pkg.getDetail()).isEqualTo("json"); + assertThat(pkg.toString()).isEqualTo(packageName); + assertThat(pkg.isValid()).isTrue(); + } + + /** + * Test of {@link Devon4jPackage#of(String)} with + * {@code com.devonfw.gastronomy.restaurant.offermanagement.dataaccess.impl.dao}. + */ + @Test + public void testOfStringSampleApp() { + + String packageName = "com.devonfw.gastronomy.restaurant.offermanagement.dataaccess.base"; + Devon4jPackage pkg = Devon4jPackage.of(packageName); + assertThat(pkg.getRoot()).isEqualTo("com.devonfw.gastronomy"); + assertThat(pkg.getApplication()).isEqualTo("restaurant"); + assertThat(pkg.getComponent()).isEqualTo("offermanagement"); + assertThat(pkg.getLayer()).isEqualTo("dataaccess"); + assertThat(pkg.isLayerBatch()).isFalse(); + assertThat(pkg.isLayerClient()).isFalse(); + assertThat(pkg.isLayerCommon()).isFalse(); + assertThat(pkg.isLayerDataAccess()).isTrue(); + assertThat(pkg.isLayerLogic()).isFalse(); + assertThat(pkg.isLayerService()).isFalse(); + assertThat(pkg.getScope()).isEqualTo("base"); + assertThat(pkg.isScopeApi()).isFalse(); + assertThat(pkg.isScopeBase()).isTrue(); + assertThat(pkg.isScopeImpl()).isFalse(); + assertThat(pkg.isScopeBase()).isTrue(); + assertThat(pkg.getDetail()).isNull(); + assertThat(pkg.toString()).isEqualTo(packageName); + assertThat(pkg.isValid()).isTrue(); + } + + /** + * Test of {@link Devon4jPackage#of(String, String, String, String, String, String)} with + * {@code com.devonfw.gastronomy.restaurant.offermanagement.dataaccess.impl.dao}. + */ + @Test + public void testOfSegmentsSampleApp() { + + String root = "com.devonfw.gastronomy"; + String app = "restaurant"; + String component = "offermanagement"; + String layer = "dataaccess"; + String scope = "base"; + Devon4jPackage pkg = Devon4jPackage.of(root, app, component, layer, scope, null); + assertThat(pkg.getRoot()).isEqualTo(root); + assertThat(pkg.getApplication()).isEqualTo(app); + assertThat(pkg.getComponent()).isEqualTo(component); + assertThat(pkg.getLayer()).isEqualTo(layer); + assertThat(pkg.isLayerBatch()).isFalse(); + assertThat(pkg.isLayerClient()).isFalse(); + assertThat(pkg.isLayerCommon()).isFalse(); + assertThat(pkg.isLayerDataAccess()).isTrue(); + assertThat(pkg.isLayerLogic()).isFalse(); + assertThat(pkg.isLayerService()).isFalse(); + assertThat(pkg.getScope()).isEqualTo(scope); + assertThat(pkg.isScopeApi()).isFalse(); + assertThat(pkg.isScopeBase()).isTrue(); + assertThat(pkg.isScopeImpl()).isFalse(); + assertThat(pkg.getDetail()).isNull(); + assertThat(pkg.toString()).isEqualTo("com.devonfw.gastronomy.restaurant.offermanagement.dataaccess.base"); + assertThat(pkg.isValid()).isTrue(); + } + + /** + * Test of {@link Devon4jPackage#of(String)} with a package-name not strictly following the conventions. + */ + @Test + public void testOfStringFallback() { + + String packageName = "com.company.sales.shop.offermanagement.data.api.dataaccess"; + Devon4jPackage pkg = Devon4jPackage.of(packageName); + assertThat(pkg.getRoot()).isEqualTo("com.company.sales"); + assertThat(pkg.getApplication()).isEqualTo("shop"); + assertThat(pkg.getComponent()).isEqualTo("offermanagement"); + assertThat(pkg.getLayer()).isEqualTo("data"); + assertThat(pkg.isLayerDataAccess()).isFalse(); + assertThat(pkg.getScope()).isEqualTo("api"); + assertThat(pkg.isScopeApi()).isTrue(); + assertThat(pkg.getDetail()).isEqualTo("dataaccess"); + assertThat(pkg.toString()).isEqualTo(packageName); + assertThat(pkg.isValid()).isFalse(); + } + + /** + * Test of {@link Devon4jPackage#of(String)} with an invalid package. + */ + @Test + public void testOfStringInvalid() { + + String packageName = "java.nio.channels.spi"; + Devon4jPackage pkg = Devon4jPackage.of(packageName); + assertThat(pkg.getRoot()).isEqualTo(packageName); + assertThat(pkg.getApplication()).isNull(); + assertThat(pkg.getComponent()).isNull(); + assertThat(pkg.getLayer()).isNull(); + assertThat(pkg.getScope()).isNull(); + assertThat(pkg.isScopeApi()).isFalse(); + assertThat(pkg.getDetail()).isNull(); + assertThat(pkg.toString()).isEqualTo(packageName); + assertThat(pkg.isValid()).isFalse(); + } + + /** + * Test of {@link Devon4jPackage#of(String)} with an illegal package. + */ + @Test(expected = IllegalArgumentException.class) + public void testOfStringIllegal() { + + String packageName = "...batch.api.impl"; + Devon4jPackage.of(packageName); + } + + /** + * Test of {@link Devon4jPackage#of(Package)} with the {@link Package} of {@link Devon4jPackage} itself. + */ + @Test + public void testOfPackage() { + + Package javaPackage = Devon4jPackage.class.getPackage(); + Devon4jPackage pkg = Devon4jPackage.of(javaPackage); + assertThat(pkg.getRoot()).isEqualTo("com.devonfw"); + assertThat(pkg.getApplication()).isEqualTo("module"); + assertThat(pkg.getComponent()).isEqualTo("basic"); + assertThat(pkg.getLayer()).isEqualTo("common"); + assertThat(pkg.isLayerCommon()).isTrue(); + assertThat(pkg.getScope()).isEqualTo("api"); + assertThat(pkg.isScopeApi()).isTrue(); + assertThat(pkg.getDetail()).isEqualTo("reflect"); + assertThat(pkg.toString()).isEqualTo(javaPackage.getName()); + assertThat(pkg.isValid()).isTrue(); + } + +} diff --git a/modules/batch/pom.xml b/modules/batch/pom.xml new file mode 100644 index 00000000..4d1643e4 --- /dev/null +++ b/modules/batch/pom.xml @@ -0,0 +1,62 @@ + + 4.0.0 + + com.devonfw.java.dev + devon4j-modules + dev-SNAPSHOT + + + com.devonfw.java.modules + devon4j-batch + ${devon4j.version} + jar + ${project.artifactId} + Batch infrastructure of the Open Application Standard Platform for Java (devon4j). + + + + + com.devonfw.java.modules + devon4j-jpa + + + com.devonfw.java.modules + devon4j-logging + + + + + + org.springframework.batch + spring-batch-core + + + org.springframework.batch + spring-batch-infrastructure + + + org.springframework.batch + spring-batch-integration + + + + + org.springframework.boot + spring-boot + true + + + + + org.springframework.batch + spring-batch-test + test + + + com.devonfw.java.modules + devon4j-test + test + + + diff --git a/modules/batch/src/main/java/com/devonfw/module/batch/common/base/SpringBootBatchCommandLine.java b/modules/batch/src/main/java/com/devonfw/module/batch/common/base/SpringBootBatchCommandLine.java new file mode 100644 index 00000000..d3a46152 --- /dev/null +++ b/modules/batch/src/main/java/com/devonfw/module/batch/common/base/SpringBootBatchCommandLine.java @@ -0,0 +1,295 @@ +package com.devonfw.module.batch.common.base; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.configuration.JobLocator; +import org.springframework.batch.core.converter.DefaultJobParametersConverter; +import org.springframework.batch.core.converter.JobParametersConverter; +import org.springframework.batch.core.launch.JobExecutionNotRunningException; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.launch.JobOperator; +import org.springframework.batch.core.launch.support.CommandLineJobRunner; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.ExitCodeGenerator; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.StringUtils; + +/** + * Launcher for launching batch jobs from the command line when Spring Boot is used. Similar to the + * {@link CommandLineJobRunner}, which does not work very well with Spring Boot. + *

+ * Do not use this class if Spring Boot is not used! + *

+ * It expects the full class name of the Spring Boot configuration class to be used as first argument, the class/XML + * file for configuring the job as second argument and the job name as third.
+ * Moreover parameters can be specified as further arguments (convention: key1=value1 key2=value2 ...). + *

+ * Example:
+ * java com.devonfw.module.batch.common.base.SpringBootBatchCommandLine com.devonfw.gastronomy.restaurant.SpringBootBatchApp + * classpath:config/app/batch/beans-productimport.xml productImportJob drinks.file=file:import/drinks.csv + * date(date)=2015/12/20 + *

+ * For stopping all running executions of a job, use the -stop option. + *

+ * Example:
+ * java com.devonfw.module.batch.common.base.SpringBootBatchCommandLine com.devonfw.gastronomy.restaurant.SpringBootBatchApp + * classpath:config/app/batch/beans-productimport.xml productImportJob -stop + * + * + */ +public class SpringBootBatchCommandLine { + + private static final Logger LOG = LoggerFactory.getLogger(SpringBootBatchCommandLine.class); + + private ResourceLoader resourceLoader = new DefaultResourceLoader(); + + public static enum Operation { + START, STOP + }; + + private JobLauncher launcher; + + private JobLocator locator; + + private JobParametersConverter parametersConverter; + + private JobOperator operator; + + public static void main(String[] args) throws Exception { + + if (args.length < 3) { + + handleIncorrectParameters(); + return; + } + + List configurations = new ArrayList<>(2); + configurations.add(args[0]); + configurations.add(args[1]); + + List parameters = new ArrayList<>(); + + Operation op = Operation.START; + if (args.length > 3 && args[3].equalsIgnoreCase("-stop")) { + + if (args.length > 4) { + + handleIncorrectParameters(); + return; + } + + op = Operation.STOP; + } else { + + for (int i = 3; i < args.length; i++) { + + parameters.add(args[i]); + } + } + + new SpringBootBatchCommandLine().execute(op, configurations, args[2], parameters); + } + + private static void handleIncorrectParameters() { + + LOG.error("Incorrect parameters."); + LOG.info("Usage:"); + LOG.info("java com.devonfw.module.batch.common.base.SpringBootBatchCommandLine" + + " " + " param1=value1 param2=value2 ..."); + LOG.info("For stopping all running executions of a batch job:"); + LOG.info("java com.devonfw.module.batch.common.base.BatchCommandLine" + + " " + " -stop"); + LOG.info("Example:"); + LOG.info("java com.devonfw.module.batch.common.base.SpringBootBatchCommandLine" + + " com.devonfw.gastronomy.restaurant.SpringBootBatchApp" + " classpath:config/app/batch/beans-productimport.xml" + + " productImportJob drinks.file=file:import/drinks.csv" + " date(date)=2015/12/20"); + } + + protected int getReturnCode(JobExecution jobExecution) { + + if (jobExecution.getStatus() != null && jobExecution.getStatus() == BatchStatus.COMPLETED) + return 0; + else + return 1; + } + + private Object getConfiguration(String stringRepresentation) { + + // try to load a source of Spring bean definitions: + // 1. try to load it as a (JavaConfig) class + // 2. if that fails: try to load it as XML resource + + try { + + return Class.forName(stringRepresentation); + } catch (ClassNotFoundException e) { + + return this.resourceLoader.getResource(stringRepresentation); + } + } + + private void findBeans(ConfigurableApplicationContext ctx) { + + this.launcher = ctx.getBean(JobLauncher.class); + this.locator = ctx.getBean(JobLocator.class); // supertype of JobRegistry + this.operator = ctx.getBean(JobOperator.class); + try { + + this.parametersConverter = ctx.getBean(JobParametersConverter.class); + } catch (NoSuchBeanDefinitionException e) { + + this.parametersConverter = new DefaultJobParametersConverter(); + } + } + + /** + * Initialize the application context and execute the operation. + *

+ * The application context is closed after the operation has finished. + * + * @param operation The operation to start. + * @param configurations The sources of bean configurations (either JavaConfig classes or XML files). + * @param jobName The name of the job to launch/stop. + * @param parameters The parameters (key=value). + * @throws Exception + */ + public void execute(Operation operation, List configurations, String jobName, List parameters) + throws Exception { + + // get sources of configuration + Class[] configurationClasses = new Class[configurations.size()]; + for (int i = 0; i < configurations.size(); i++) { + + configurationClasses[i] = Class.forName(configurations.get(i)); + } + + SpringApplication app = new SpringApplication(configurationClasses); + + // no (web) server needed + app.setWebEnvironment(false); + + // start the application + ConfigurableApplicationContext ctx = app.run(new String[0]); + + switch (operation) { + case START: + startBatch(ctx, jobName, parameters); + break; + case STOP: + stopBatch(ctx, jobName); + break; + default: + throw new RuntimeException("Unknown operation: " + operation); + } + + } + + private void startBatch(ConfigurableApplicationContext ctx, String jobName, List parameters) + throws Exception { + + JobExecution jobExecution = null; + try { + + findBeans(ctx); + + JobParameters params = this.parametersConverter + .getJobParameters(StringUtils.splitArrayElementsIntoProperties(parameters.toArray(new String[] {}), "=")); + + // execute the batch + // the JobOperator would require special logic for a restart, so we + // are using the JobLauncher directly here + jobExecution = this.launcher.run(this.locator.getJob(jobName), params); + + } finally { + + // evaluate the outcome + final int returnCode = (jobExecution == null) ? 1 : getReturnCode(jobExecution); + if (jobExecution == null) { + + LOG.error("Batch Status: Batch could not be started."); + } else { + + LOG.info("Batch start time: {}", jobExecution.getStartTime() == null ? "null" : jobExecution.getStartTime()); + LOG.info("Batch end time: {}", jobExecution.getEndTime() == null ? "null" : jobExecution.getEndTime()); + + if (returnCode == 0) { + + LOG.info("Batch Status: {}", jobExecution.getStatus() == null ? "null" : jobExecution.getStatus()); + } else { + + LOG.error("Batch Status: {}", jobExecution.getStatus() == null ? "null" : jobExecution.getStatus()); + } + } + LOG.info("Return Code: {}", returnCode); + + SpringApplication.exit(ctx, new ExitCodeGenerator() { + + @Override + public int getExitCode() { + + return returnCode; + } + }); + } + } + + private void stopBatch(ConfigurableApplicationContext ctx, String jobName) throws Exception { + + int returnCode = 0; + try { + + findBeans(ctx); + + Set runningJobExecutionIDs = this.operator.getRunningExecutions(jobName); + if (runningJobExecutionIDs.isEmpty()) { + + throw new JobExecutionNotRunningException("Batch job " + jobName + " is currently not being executed."); + } + + LOG.debug("Found {} executions to be stopped (potentially" + " already in state stopping).", + runningJobExecutionIDs.size()); + + int stoppedCount = 0; + for (Long id : runningJobExecutionIDs) { + + try { + + this.operator.stop(id); + stoppedCount++; + } catch (JobExecutionNotRunningException e) { + + // might have finished at this point + // or was in state stopping already + } + } + + LOG.info("Actually stopped {} batch executions.", stoppedCount); + + } catch (Exception e) { + + returnCode = 1; + throw e; + } finally { + + final int returnCodeResult = returnCode; + SpringApplication.exit(ctx, new ExitCodeGenerator() { + + @Override + public int getExitCode() { + + return returnCodeResult; + } + }); + } + } +} diff --git a/modules/batch/src/main/java/com/devonfw/module/batch/common/impl/ChunkLoggingListener.java b/modules/batch/src/main/java/com/devonfw/module/batch/common/impl/ChunkLoggingListener.java new file mode 100644 index 00000000..88070e31 --- /dev/null +++ b/modules/batch/src/main/java/com/devonfw/module/batch/common/impl/ChunkLoggingListener.java @@ -0,0 +1,93 @@ +package com.devonfw.module.batch.common.impl; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.batch.core.ItemProcessListener; +import org.springframework.batch.core.ItemReadListener; +import org.springframework.batch.core.ItemWriteListener; +import org.springframework.batch.core.SkipListener; + +/** + * Spring Batch listener that logs exceptions together with the item(s) being processed at the time the exceptions + * occurred. + * + */ +public class ChunkLoggingListener implements SkipListener, ItemReadListener, ItemProcessListener, + ItemWriteListener { + + private static final Logger LOG = LoggerFactory.getLogger(ChunkLoggingListener.class); + + protected String itemToString(Object item) { + + return item.toString(); + } + + @Override + public void onReadError(Exception e) { + + LOG.error("Failed to read item.", e); + } + + @Override + public void onProcessError(T item, Exception e) { + + LOG.error("Failed to process item: " + itemToString(item), e); + } + + @Override + public void onWriteError(Exception e, List items) { + + LOG.error("Failed to write items: " + itemToString(items), e); + } + + @Override + public void onSkipInRead(Throwable t) { + + LOG.warn("Skipped item in read.", t); + } + + @Override + public void onSkipInProcess(T item, Throwable t) { + + LOG.warn("Skipped item in process: " + itemToString(item), t); + } + + @Override + public void onSkipInWrite(S item, Throwable t) { + + LOG.warn("Skipped item in write: " + itemToString(item), t); + } + + @Override + public void beforeRead() { + + } + + @Override + public void afterRead(T item) { + + } + + @Override + public void beforeProcess(T item) { + + } + + @Override + public void afterProcess(T item, S result) { + + } + + @Override + public void beforeWrite(List items) { + + } + + @Override + public void afterWrite(List items) { + + } + +} \ No newline at end of file diff --git a/modules/batch/src/main/java/com/devonfw/module/batch/common/impl/JobLauncherWithAdditionalRestartCapabilities.java b/modules/batch/src/main/java/com/devonfw/module/batch/common/impl/JobLauncherWithAdditionalRestartCapabilities.java new file mode 100644 index 00000000..2ae90511 --- /dev/null +++ b/modules/batch/src/main/java/com/devonfw/module/batch/common/impl/JobLauncherWithAdditionalRestartCapabilities.java @@ -0,0 +1,67 @@ +package com.devonfw.module.batch.common.impl; + +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersIncrementer; +import org.springframework.batch.core.JobParametersInvalidException; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.launch.support.SimpleJobLauncher; +import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException; +import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.repository.JobRestartException; + +/** + * {@link JobLauncher} that extends the functionality provided by the standard {@link SimpleJobLauncher}: + *

+ * For batches, which always restart from scratch (i.e. those marked with restartable="false"), the parameter's are + * 'incremented' automatically using the {@link JobParametersIncrementer} from the job (usually by adding or modifying + * the 'run.id' parameter). It is actually just a convenience functionality so that the one starting batches does not + * have to change the parameters manually. + * + */ +public class JobLauncherWithAdditionalRestartCapabilities extends SimpleJobLauncher { + + private JobRepository jobRepository; + + @Override + public JobExecution run(final Job job, JobParameters jobParameters) throws JobExecutionAlreadyRunningException, + JobRestartException, JobInstanceAlreadyCompleteException, JobParametersInvalidException { + + if (!job.isRestartable()) { + + JobParameters originalParameters = jobParameters; + while (this.jobRepository.isJobInstanceExists(job.getName(), jobParameters)) { + + // check if batch job is still running or was completed already + // analogous to SimpleJobRepository#createJobExecution + JobExecution jobExecution = this.jobRepository.getLastJobExecution(job.getName(), jobParameters); + if (jobExecution.isRunning()) { + throw new JobExecutionAlreadyRunningException("A job execution for this job is already running: " + + jobExecution.getJobInstance()); + } + BatchStatus status = jobExecution.getStatus(); + if (status == BatchStatus.COMPLETED || status == BatchStatus.ABANDONED) { + throw new JobInstanceAlreadyCompleteException("A job instance already exists and is complete for parameters=" + + originalParameters + ". If you want to run this job again, change the parameters."); + } + + // if there is a NullPointerException executing the following statement + // there has not been a JobParametersIncrementer set for the job + jobParameters = job.getJobParametersIncrementer().getNext(jobParameters); + } + } + + return super.run(job, jobParameters); + } + + @Override + public void setJobRepository(JobRepository jobRepository) { + + super.setJobRepository(jobRepository); + this.jobRepository = jobRepository; + } + +} diff --git a/modules/beanmapping/pom.xml b/modules/beanmapping/pom.xml new file mode 100644 index 00000000..e435da70 --- /dev/null +++ b/modules/beanmapping/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-modules + dev-SNAPSHOT + + com.devonfw.java.modules + devon4j-beanmapping + ${devon4j.version} + jar + ${project.artifactId} + Minimal shim for bean mapping to convert between compatible Java beans (e.g. JPA entity to transfer-object and vice versa). + + + + net.sf.dozer + dozer + + + javax.inject + javax.inject + + + net.sf.m-m-m + mmm-util-entity + + + + + ma.glasnost.orika + orika-core + true + + + + com.devonfw.java.modules + devon4j-test + test + + + + \ No newline at end of file diff --git a/modules/beanmapping/src/main/java/com/devonfw/module/beanmapping/common/api/BeanMapper.java b/modules/beanmapping/src/main/java/com/devonfw/module/beanmapping/common/api/BeanMapper.java new file mode 100644 index 00000000..66f254cd --- /dev/null +++ b/modules/beanmapping/src/main/java/com/devonfw/module/beanmapping/common/api/BeanMapper.java @@ -0,0 +1,93 @@ +package com.devonfw.module.beanmapping.common.api; + +import java.util.List; +import java.util.Set; + +/** + * This is the interface used to convert from one Java bean to another compatible bean (e.g. from a JPA entity to a + * corresponding transfer-object). + * + */ +public interface BeanMapper { + + /** + * Recursively converts the given source {@link Object} to the given target {@link Class}. + * + * @param is the generic type to convert to. + * @param source is the object to convert. + * @param targetClass is the {@link Class} reflecting the type to convert to. + * @return the converted object. Will be {@code null} if source is {@code null}. + */ + T map(Object source, Class targetClass); + + /** + * A type-safe variant of {@link #map(Object, Class)} to prevent accidental abuse (e.g. mapping of apples to bananas). + * + * @param is a common super-type (interface) of source and targetType. + * @param is the generic type of source. + * @param is the generic type to convert to (target). + * @param apiClass is the {@link Class} reflecting the {@literal }. + * @param source is the object to convert. + * @param targetClass is the {@link Class} reflecting the type to convert to. + * @return the converted object. Will be {@code null} if source is {@code null}. + * @since 1.3.0 + */ + T mapTypesafe(Class apiClass, S source, Class targetClass); + + /** + * Creates a new {@link List} with the {@link #map(Object, Class) mapped bean} for each {@link List#get(int) entry} of + * the given {@link List}. Uses {@code false} for suppressNullValues (see + * {@link #mapList(List, Class, boolean)}). + * + * @param is the generic type to convert the {@link List} entries to. + * @param source is the {@link List} with the source objects. + * @param targetClass is the {@link Class} reflecting the type to convert each {@link List} entry to. + * @return the {@link List} with the converted objects. Will be {@link List#isEmpty() empty} is source is + * empty or {@code null}. + */ + List mapList(List source, Class targetClass); + + /** + * Creates a new {@link List} with the {@link #map(Object, Class) mapped bean} for each {@link List#get(int) entry} of + * the given {@link List}. + * + * @param is the generic type to convert the {@link List} entries to. + * @param source is the {@link List} with the source objects. + * @param targetClass is the {@link Class} reflecting the type to convert each {@link List} entry to. + * @param suppressNullValues {@code true} if {@code null} values shall be suppressed/omitted in the + * resulting {@link List}, {@code false} otherwise. + * @return the {@link List} with the converted objects. Will be {@link List#isEmpty() empty} is source is + * empty or {@code null}. + * @since 1.3.0 + */ + List mapList(List source, Class targetClass, boolean suppressNullValues); + + /** + * Creates a new {@link Set} with the {@link #map(Object, Class) mapped bean} for each {@link Set#contains(Object) + * entry} of the given {@link Set}. Uses {@code false} for suppressNullValues (see + * {@link #mapSet(Set, Class, boolean)}). + * + * @param is the generic type to convert the {@link Set} entries to. + * @param source is the {@link Set} with the source objects. + * @param targetClass is the {@link Class} reflecting the type to convert each {@link Set} entry to. + * @return the {@link Set} with the converted objects. Will be {@link Set#isEmpty() empty} is source is + * empty or {@code null}. + */ + Set mapSet(Set source, Class targetClass); + + /** + * Creates a new {@link Set} with the {@link #map(Object, Class) mapped bean} for each {@link Set#contains(Object) + * entry} of the given {@link Set}. + * + * @param is the generic type to convert the {@link Set} entries to. + * @param source is the {@link Set} with the source objects. + * @param targetClass is the {@link Class} reflecting the type to convert each {@link Set} entry to. + * @param suppressNullValues {@code true} if {@code null} values shall be suppressed/omitted in the + * resulting {@link Set}, {@code false} otherwise. + * @return the {@link Set} with the converted objects. Will be {@link Set#isEmpty() empty} is source is + * empty or {@code null}. + * @since 1.3.0 + */ + Set mapSet(Set source, Class targetClass, boolean suppressNullValues); + +} diff --git a/modules/beanmapping/src/main/java/com/devonfw/module/beanmapping/common/base/AbstractBeanMapper.java b/modules/beanmapping/src/main/java/com/devonfw/module/beanmapping/common/base/AbstractBeanMapper.java new file mode 100644 index 00000000..e5684cb3 --- /dev/null +++ b/modules/beanmapping/src/main/java/com/devonfw/module/beanmapping/common/base/AbstractBeanMapper.java @@ -0,0 +1,71 @@ +package com.devonfw.module.beanmapping.common.base; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.devonfw.module.beanmapping.common.api.BeanMapper; + +/** + * The abstract base implementation of {@link BeanMapper}. + * + */ +public abstract class AbstractBeanMapper implements BeanMapper { + + /** + * The constructor. + */ + public AbstractBeanMapper() { + + super(); + } + + @Override + public T mapTypesafe(Class apiClass, S source, Class targetClass) { + + return map(source, targetClass); + } + + @Override + public List mapList(List source, Class targetClass) { + + return mapList(source, targetClass, false); + } + + @Override + public List mapList(List source, Class targetClass, boolean suppressNullValues) { + + if ((source == null) || (source.isEmpty())) { + return new ArrayList<>(); + } + List result = new ArrayList<>(source.size()); + for (Object sourceObject : source) { + if ((sourceObject != null) || !suppressNullValues) { + result.add(map(sourceObject, targetClass)); + } + } + return result; + } + + @Override + public Set mapSet(Set source, Class targetClass) { + + return mapSet(source, targetClass, false); + } + + @Override + public Set mapSet(Set source, Class targetClass, boolean suppressNullValues) { + + if ((source == null) || (source.isEmpty())) { + return new HashSet<>(); + } + Set result = new HashSet<>(source.size()); + for (Object sourceObject : source) { + if ((sourceObject != null) || !suppressNullValues) { + result.add(map(sourceObject, targetClass)); + } + } + return result; + } +} diff --git a/modules/beanmapping/src/main/java/com/devonfw/module/beanmapping/common/impl/DozerBeanMapper.java b/modules/beanmapping/src/main/java/com/devonfw/module/beanmapping/common/impl/DozerBeanMapper.java new file mode 100644 index 00000000..57993e9c --- /dev/null +++ b/modules/beanmapping/src/main/java/com/devonfw/module/beanmapping/common/impl/DozerBeanMapper.java @@ -0,0 +1,24 @@ +package com.devonfw.module.beanmapping.common.impl; + +import org.dozer.Mapper; + +import com.devonfw.module.beanmapping.common.impl.dozer.BeanMapperImplDozer; + +/** + * This is the implementation of {@link com.devonfw.module.beanmapping.common.api.BeanMapper} using dozer {@link Mapper}. + * + * @deprecated - use {@link BeanMapperImplDozer} instead as this class name clashes with + * {@link org.dozer.DozerBeanMapper}. + */ +@Deprecated +public class DozerBeanMapper extends BeanMapperImplDozer { + + /** + * The constructor. + */ + public DozerBeanMapper() { + + super(); + } + +} diff --git a/modules/beanmapping/src/main/java/com/devonfw/module/beanmapping/common/impl/IdentityConverter.java b/modules/beanmapping/src/main/java/com/devonfw/module/beanmapping/common/impl/IdentityConverter.java new file mode 100644 index 00000000..27c68ae1 --- /dev/null +++ b/modules/beanmapping/src/main/java/com/devonfw/module/beanmapping/common/impl/IdentityConverter.java @@ -0,0 +1,29 @@ +package com.devonfw.module.beanmapping.common.impl; + +import org.dozer.CustomConverter; + +/** + * Dozer {@link CustomConverter} that returns the original source object reference (identity conversion). + * + * @deprecated - use {@link com.devonfw.module.beanmapping.common.impl.dozer.IdentityConverter} + */ +@Deprecated +public class IdentityConverter implements CustomConverter { + + /** + * The constructor. + */ + public IdentityConverter() { + + super(); + System.err.println("This class is deprecated. Please use " + + com.devonfw.module.beanmapping.common.impl.dozer.IdentityConverter.class.getName()); + } + + @Override + public Object convert(Object destination, Object source, Class destinationClass, Class sourceClass) { + + return source; + } + +} diff --git a/modules/beanmapping/src/main/java/com/devonfw/module/beanmapping/common/impl/dozer/BeanMapperImplDozer.java b/modules/beanmapping/src/main/java/com/devonfw/module/beanmapping/common/impl/dozer/BeanMapperImplDozer.java new file mode 100644 index 00000000..5cac7191 --- /dev/null +++ b/modules/beanmapping/src/main/java/com/devonfw/module/beanmapping/common/impl/dozer/BeanMapperImplDozer.java @@ -0,0 +1,47 @@ +package com.devonfw.module.beanmapping.common.impl.dozer; + +import javax.inject.Inject; +import javax.inject.Named; + +import org.dozer.Mapper; + +import com.devonfw.module.beanmapping.common.base.AbstractBeanMapper; + +/** + * This is the implementation of {@link com.devonfw.module.beanmapping.common.api.BeanMapper} using dozer {@link Mapper}. + * + * @since 1.3.0 + */ +@Named +public class BeanMapperImplDozer extends AbstractBeanMapper { + + /** The dozer instance to use. */ + private Mapper dozer; + + /** + * The constructor. + */ + public BeanMapperImplDozer() { + + super(); + } + + /** + * @param dozer is the {@link Mapper} to {@link Inject}. + */ + @Inject + public void setDozer(Mapper dozer) { + + this.dozer = dozer; + } + + @Override + public T map(Object source, Class targetClass) { + + if (source == null) { + return null; + } + return this.dozer.map(source, targetClass); + } + +} diff --git a/modules/beanmapping/src/main/java/com/devonfw/module/beanmapping/common/impl/dozer/IdentityConverter.java b/modules/beanmapping/src/main/java/com/devonfw/module/beanmapping/common/impl/dozer/IdentityConverter.java new file mode 100644 index 00000000..5a62a579 --- /dev/null +++ b/modules/beanmapping/src/main/java/com/devonfw/module/beanmapping/common/impl/dozer/IdentityConverter.java @@ -0,0 +1,25 @@ +package com.devonfw.module.beanmapping.common.impl.dozer; + +import org.dozer.CustomConverter; + +/** + * Dozer {@link CustomConverter} that returns the original source object reference (identity conversion). + * + */ +public class IdentityConverter implements CustomConverter { + + /** + * The constructor. + */ + public IdentityConverter() { + + super(); + } + + @Override + public Object convert(Object destination, Object source, Class destinationClass, Class sourceClass) { + + return source; + } + +} diff --git a/modules/beanmapping/src/main/java/com/devonfw/module/beanmapping/common/impl/orika/BeanMapperImplOrika.java b/modules/beanmapping/src/main/java/com/devonfw/module/beanmapping/common/impl/orika/BeanMapperImplOrika.java new file mode 100644 index 00000000..a11d8bde --- /dev/null +++ b/modules/beanmapping/src/main/java/com/devonfw/module/beanmapping/common/impl/orika/BeanMapperImplOrika.java @@ -0,0 +1,44 @@ +package com.devonfw.module.beanmapping.common.impl.orika; + +import javax.inject.Inject; + +import com.devonfw.module.beanmapping.common.base.AbstractBeanMapper; + +import ma.glasnost.orika.MapperFacade; + +/** + * This is the implementation of {@link com.devonfw.module.beanmapping.common.api.BeanMapper} using orika + * {@link MapperFacade}. + * + */ +public class BeanMapperImplOrika extends AbstractBeanMapper { + + private MapperFacade orika; + + /** + * The constructor. + */ + public BeanMapperImplOrika() { + + super(); + } + + /** + * @param orika the orika to set + */ + @Inject + public void setOrika(MapperFacade orika) { + + this.orika = orika; + } + + @Override + public T map(Object source, Class targetClass) { + + if (source == null) { + return null; + } + return this.orika.map(source, targetClass); + } + +} diff --git a/modules/beanmapping/src/main/java/com/devonfw/module/beanmapping/common/impl/orika/CustomMapperEto.java b/modules/beanmapping/src/main/java/com/devonfw/module/beanmapping/common/impl/orika/CustomMapperEto.java new file mode 100644 index 00000000..ef556f9b --- /dev/null +++ b/modules/beanmapping/src/main/java/com/devonfw/module/beanmapping/common/impl/orika/CustomMapperEto.java @@ -0,0 +1,71 @@ +package com.devonfw.module.beanmapping.common.impl.orika; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; + +import ma.glasnost.orika.CustomMapper; +import ma.glasnost.orika.MappingContext; + +import net.sf.mmm.util.entity.api.GenericEntity; +import net.sf.mmm.util.entity.api.PersistenceEntity; +import net.sf.mmm.util.pojo.descriptor.api.PojoDescriptor; +import net.sf.mmm.util.pojo.descriptor.api.PojoDescriptorBuilder; +import net.sf.mmm.util.pojo.descriptor.api.PojoDescriptorBuilderFactory; +import net.sf.mmm.util.pojo.descriptor.impl.PojoDescriptorBuilderFactoryImpl; +import net.sf.mmm.util.transferobject.api.EntityTo; + +/** + * {@link CustomMapper} to map from {@link PersistenceEntity} to {@link EntityTo} to solve + * {@link EntityTo#getModificationCounter() modification counter issue}. + * + */ +// @Named +@SuppressWarnings("rawtypes") +public class CustomMapperEto extends CustomMapper { + + private PojoDescriptorBuilder pojoDescriptorBuilder; + + private PojoDescriptorBuilderFactory pojoDescriptorBuilderFactory; + + private PojoDescriptor descriptor; + + /** + * The constructor. + */ + public CustomMapperEto() { + + super(); + + } + + @Override + public void mapAtoB(GenericEntity source, EntityTo target, MappingContext context) { + + this.descriptor.setProperty(target, "persistentEntity", source); + } + + /** + * Initializes this class to be functional. + */ + @PostConstruct + public void initialize() { + + if (this.pojoDescriptorBuilderFactory == null) { + this.pojoDescriptorBuilderFactory = PojoDescriptorBuilderFactoryImpl.getInstance(); + } + if (this.pojoDescriptorBuilder == null) { + this.pojoDescriptorBuilder = this.pojoDescriptorBuilderFactory.createPrivateFieldDescriptorBuilder(); + } + this.descriptor = this.pojoDescriptorBuilder.getDescriptor(EntityTo.class); + } + + /** + * @param pojoDescriptorBuilderFactory the pojoDescriptorBuilderFactory to set + */ + @Inject + public void setPojoDescriptorBuilderFactory(PojoDescriptorBuilderFactory pojoDescriptorBuilderFactory) { + + this.pojoDescriptorBuilderFactory = pojoDescriptorBuilderFactory; + } + +} diff --git a/modules/beanmapping/src/test/java/com/devonfw/module/beanmapping/common/impl/AbstractBeanMapperTest.java b/modules/beanmapping/src/test/java/com/devonfw/module/beanmapping/common/impl/AbstractBeanMapperTest.java new file mode 100644 index 00000000..15c8fa00 --- /dev/null +++ b/modules/beanmapping/src/test/java/com/devonfw/module/beanmapping/common/impl/AbstractBeanMapperTest.java @@ -0,0 +1,121 @@ +package com.devonfw.module.beanmapping.common.impl; + +import net.sf.mmm.util.entity.api.PersistenceEntity; +import net.sf.mmm.util.entity.api.RevisionedEntity; +import net.sf.mmm.util.entity.base.AbstractRevisionedEntity; +import net.sf.mmm.util.transferobject.api.EntityTo; + +import org.junit.Test; + +import com.devonfw.module.beanmapping.common.api.BeanMapper; +import com.devonfw.module.test.common.base.ModuleTest; + +/** + * The abstract test-case for testing {@link BeanMapper} via its interface. + * + */ +public abstract class AbstractBeanMapperTest extends ModuleTest { + + /** + * @return the {@link BeanMapper} instance to test. + */ + protected abstract BeanMapper getBeanMapper(); + + /** + * Tests {@link BeanMapper#mapTypesafe(Class, Object, Class)} for an {@link PersistenceEntity entity} to an + * {@link EntityTo ETO} and ensures that if the {@link PersistenceEntity#getModificationCounter() modification + * counter} gets updated after conversion that the {@link EntityTo ETO} reflects this change. + */ + @Test + public void testMapEntity2Eto() { + + // given + BeanMapper mapper = getBeanMapper(); + MyBeanEntity entity = new MyBeanEntity(); + Long id = 1L; + entity.setId(id); + int version = 1; + entity.setModificationCounter(version); + Number revision = 10L; + entity.setRevision(revision); + String property = "its magic"; + entity.setProperty(property); + + // when + MyBeanEto eto = mapper.mapTypesafe(MyBean.class, entity, MyBeanEto.class); + + // then + assertThat(eto).isNotNull(); + assertThat(eto.getId()).isEqualTo(id); + assertThat(eto.getModificationCounter()).isEqualTo(version); + assertThat(eto.getRevision()).isEqualTo(revision); + assertThat(eto.getProperty()).isEqualTo(property); + // sepcial feature: update of modificationCounter is performed when TX is closed what is typically after conversion + int newVersion = version + 1; + entity.setModificationCounter(newVersion); + assertThat(eto.getModificationCounter()).isEqualTo(newVersion); + } + + /** + * Interface for {@link MyBeanEntity} and {@link MyBeanEto}. + */ + public static interface MyBean extends RevisionedEntity { + + /** + * @return property + */ + String getProperty(); + + /** + * @param property the property to set + */ + void setProperty(String property); + + } + + /** + * {@link PersistenceEntity} for testing. + */ + public static class MyBeanEntity extends AbstractRevisionedEntity implements PersistenceEntity, MyBean { + + private static final long serialVersionUID = 1L; + + private String property; + + @Override + public String getProperty() { + + return this.property; + } + + @Override + public void setProperty(String property) { + + this.property = property; + } + + } + + /** + * {@link EntityTo ETO} for testing. + */ + public static class MyBeanEto extends EntityTo implements MyBean { + + private static final long serialVersionUID = 1L; + + private String property; + + @Override + public String getProperty() { + + return this.property; + } + + @Override + public void setProperty(String property) { + + this.property = property; + } + } + +} diff --git a/modules/beanmapping/src/test/java/com/devonfw/module/beanmapping/common/impl/BeanMapperImplDozerTest.java b/modules/beanmapping/src/test/java/com/devonfw/module/beanmapping/common/impl/BeanMapperImplDozerTest.java new file mode 100644 index 00000000..cd066553 --- /dev/null +++ b/modules/beanmapping/src/test/java/com/devonfw/module/beanmapping/common/impl/BeanMapperImplDozerTest.java @@ -0,0 +1,43 @@ +package com.devonfw.module.beanmapping.common.impl; + +import java.util.Arrays; +import java.util.List; + +import net.sf.mmm.util.entity.api.PersistenceEntity; +import net.sf.mmm.util.transferobject.api.EntityTo; + +import org.dozer.DozerBeanMapper; +import org.dozer.loader.api.BeanMappingBuilder; +import org.dozer.loader.api.FieldsMappingOptions; + +import com.devonfw.module.beanmapping.common.api.BeanMapper; +import com.devonfw.module.beanmapping.common.impl.dozer.BeanMapperImplDozer; +import com.devonfw.module.beanmapping.common.impl.dozer.IdentityConverter; + +/** + * Test of {@link BeanMapperImplDozer} based on {@link AbstractBeanMapperTest}. + * + */ +public class BeanMapperImplDozerTest extends AbstractBeanMapperTest { + + @Override + protected BeanMapper getBeanMapper() { + + BeanMapperImplDozer mapper = new BeanMapperImplDozer(); + List mappingFiles = Arrays.asList("config/app/common/dozer-mapping.xml"); + DozerBeanMapper dozer = new DozerBeanMapper(mappingFiles); + BeanMappingBuilder builder = new BeanMappingBuilder() { + + @Override + protected void configure() { + + mapping(PersistenceEntity.class, EntityTo.class).fields(this_(), field("persistentEntity").accessible(), + FieldsMappingOptions.customConverter(IdentityConverter.class)); + } + }; + dozer.addMapping(builder); + mapper.setDozer(dozer); + return mapper; + } + +} diff --git a/modules/beanmapping/src/test/java/com/devonfw/module/beanmapping/common/impl/BeanMapperImplOrikaTest.java b/modules/beanmapping/src/test/java/com/devonfw/module/beanmapping/common/impl/BeanMapperImplOrikaTest.java new file mode 100644 index 00000000..5865a43a --- /dev/null +++ b/modules/beanmapping/src/test/java/com/devonfw/module/beanmapping/common/impl/BeanMapperImplOrikaTest.java @@ -0,0 +1,33 @@ +package com.devonfw.module.beanmapping.common.impl; + +import ma.glasnost.orika.MapperFacade; +import ma.glasnost.orika.MapperFactory; +import ma.glasnost.orika.impl.DefaultMapperFactory; + +import net.sf.mmm.util.entity.api.GenericEntity; +import net.sf.mmm.util.transferobject.api.EntityTo; + +import com.devonfw.module.beanmapping.common.api.BeanMapper; +import com.devonfw.module.beanmapping.common.impl.orika.BeanMapperImplOrika; +import com.devonfw.module.beanmapping.common.impl.orika.CustomMapperEto; + +/** + * Test of {@link BeanMapperImplOrika} based on {@link AbstractBeanMapperTest}. + * + */ +public class BeanMapperImplOrikaTest extends AbstractBeanMapperTest { + + @Override + protected BeanMapper getBeanMapper() { + + BeanMapperImplOrika mapper = new BeanMapperImplOrika(); + MapperFactory factory = new DefaultMapperFactory.Builder().build(); + CustomMapperEto customMapper = new CustomMapperEto(); + customMapper.initialize(); + factory.classMap(GenericEntity.class, EntityTo.class).customize(customMapper).byDefault().favorExtension(true) + .register(); + MapperFacade orika = factory.getMapperFacade(); + mapper.setOrika(orika); + return mapper; + } +} diff --git a/modules/beanmapping/src/test/resources/config/app/common/dozer-mapping.xml b/modules/beanmapping/src/test/resources/config/app/common/dozer-mapping.xml new file mode 100644 index 00000000..3e71f5eb --- /dev/null +++ b/modules/beanmapping/src/test/resources/config/app/common/dozer-mapping.xml @@ -0,0 +1,18 @@ + + + + + + true + + + java.lang.String + java.lang.Long + java.lang.Integer + java.lang.Number + java.lang.Boolean + + + diff --git a/modules/configuration/pom.xml b/modules/configuration/pom.xml new file mode 100644 index 00000000..a1eab742 --- /dev/null +++ b/modules/configuration/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-modules + dev-SNAPSHOT + + com.devonfw.java.modules + devon4j-configuration + ${devon4j.version} + jar + ${project.artifactId} + Configuration Module of the Open Application Standard Platform for Java (devon4j). + + + + com.devonfw.java.modules + devon4j-test + test + + + + \ No newline at end of file diff --git a/modules/cxf-client-rest/pom.xml b/modules/cxf-client-rest/pom.xml new file mode 100644 index 00000000..b74d9ca3 --- /dev/null +++ b/modules/cxf-client-rest/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-modules + dev-SNAPSHOT + + com.devonfw.java.modules + devon4j-cxf-client-rest + ${devon4j.version} + jar + ${project.artifactId} + Support for consuming REST services as client based on Apache CXF. + + + + ${project.groupId} + devon4j-cxf-client + + + ${project.groupId} + devon4j-json + + + org.apache.cxf + cxf-rt-rs-client + + + com.devonfw.java.modules + devon4j-test + test + + + + diff --git a/modules/cxf-client-rest/src/main/java/com/devonfw/module/cxf/common/impl/client/rest/CxfRestClientAutoConfiguration.java b/modules/cxf-client-rest/src/main/java/com/devonfw/module/cxf/common/impl/client/rest/CxfRestClientAutoConfiguration.java new file mode 100644 index 00000000..0cf34be5 --- /dev/null +++ b/modules/cxf-client-rest/src/main/java/com/devonfw/module/cxf/common/impl/client/rest/CxfRestClientAutoConfiguration.java @@ -0,0 +1,25 @@ +package com.devonfw.module.cxf.common.impl.client.rest; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.devonfw.module.service.common.api.sync.SyncServiceClientFactory; + +/** + * {@link Configuration} for REST (JAX-RS) clients using Apache CXF. + * + * @since 3.0.0 + */ +@Configuration +public class CxfRestClientAutoConfiguration { + + /** + * @return an implemenation of {@link SyncServiceClientFactory} based on CXF for REST (JAX-RS). + */ + @Bean + public SyncServiceClientFactory syncServiceClientFactoryCxfRest() { + + return new SyncServiceClientFactoryCxfRest(); + } + +} diff --git a/modules/cxf-client-rest/src/main/java/com/devonfw/module/cxf/common/impl/client/rest/RestServiceExceptionMapper.java b/modules/cxf-client-rest/src/main/java/com/devonfw/module/cxf/common/impl/client/rest/RestServiceExceptionMapper.java new file mode 100644 index 00000000..a52ab07a --- /dev/null +++ b/modules/cxf-client-rest/src/main/java/com/devonfw/module/cxf/common/impl/client/rest/RestServiceExceptionMapper.java @@ -0,0 +1,104 @@ +package com.devonfw.module.cxf.common.impl.client.rest; + +import java.io.IOException; +import java.util.Map; +import java.util.UUID; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +import net.sf.mmm.util.exception.api.ServiceInvocationFailedException; + +import org.apache.cxf.jaxrs.client.ResponseExceptionMapper; + +import com.devonfw.module.service.common.api.constants.ServiceConstants; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * An Implementation of {@link ResponseExceptionMapper} that converts a REST failure {@link Response} compliant with + * OASP REST error specification + * to a {@link ServiceInvocationFailedException}. + * + * @since 3.0.0 + */ +@Provider +public class RestServiceExceptionMapper implements ResponseExceptionMapper { + + private String serviceName; + + /** + * The constructor. + * + * @param service the name (e.g. {@link Class#getName() qualified name}) of the + * {@link com.devonfw.module.service.common.api.Service} that failed. + */ + public RestServiceExceptionMapper(String service) { + + super(); + this.serviceName = service; + } + + @Override + public Throwable fromResponse(Response response) { + + response.bufferEntity(); + if (response.hasEntity()) { + String json = response.readEntity(String.class); + if ((json != null) && !json.isEmpty()) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + Map jsonMap = objectMapper.readValue(json, Map.class); + return createException(jsonMap); + } catch (IOException e) { + return new ServiceInvocationFailedTechnicalException(e, e.getMessage(), e.getClass().getSimpleName(), null, + this.serviceName); + } + } + } + return null; + } + + private Throwable createException(Map jsonMap) { + + String code = (String) jsonMap.get(ServiceConstants.KEY_CODE); + String message = (String) jsonMap.get(ServiceConstants.KEY_MESSAGE); + String uuid = (String) jsonMap.get(ServiceConstants.KEY_UUID); + + return createException(code, message, UUID.fromString(uuid)); + } + + private Throwable createException(String code, String message, UUID uuid) { + + return new ServiceInvocationFailedException(message, code, uuid, this.serviceName); + } + + /** + * Extends {@link ServiceInvocationFailedException} as {@link #isTechnical() technical} exception. + */ + private static final class ServiceInvocationFailedTechnicalException extends ServiceInvocationFailedException { + + private static final long serialVersionUID = 1L; + + /** + * The constructor. + * + * @param cause the {@link #getCause() cause} of this exception. + * @param message the {@link #getMessage() message}. + * @param code the {@link #getCode() code}. + * @param uuid {@link UUID} the {@link #getUuid() UUID}. + * @param service the name (e.g. {@link Class#getName() qualified name}) of the service that failed. + */ + private ServiceInvocationFailedTechnicalException(Throwable cause, String message, String code, UUID uuid, + String service) { + + super(cause, message, code, uuid, service); + } + + @Override + public boolean isForUser() { + + return false; + } + } + +} diff --git a/modules/cxf-client-rest/src/main/java/com/devonfw/module/cxf/common/impl/client/rest/SyncServiceClientFactoryCxfRest.java b/modules/cxf-client-rest/src/main/java/com/devonfw/module/cxf/common/impl/client/rest/SyncServiceClientFactoryCxfRest.java new file mode 100644 index 00000000..632e43c6 --- /dev/null +++ b/modules/cxf-client-rest/src/main/java/com/devonfw/module/cxf/common/impl/client/rest/SyncServiceClientFactoryCxfRest.java @@ -0,0 +1,101 @@ +package com.devonfw.module.cxf.common.impl.client.rest; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.inject.Inject; +import javax.ws.rs.Path; + +import org.apache.cxf.jaxrs.client.Client; +import org.apache.cxf.jaxrs.client.ClientConfiguration; +import org.apache.cxf.jaxrs.client.JAXRSClientFactory; +import org.apache.cxf.jaxrs.client.WebClient; + +import com.devonfw.module.cxf.common.impl.client.SyncServiceClientFactoryCxf; +import com.devonfw.module.service.common.api.client.context.ServiceContext; +import com.devonfw.module.service.common.api.constants.ServiceConstants; +import com.devonfw.module.service.common.api.sync.SyncServiceClientFactory; +import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider; + +/** + * Implementation of {@link SyncServiceClientFactory} for JAX-RS REST service clients using Apache CXF. + * + * @since 3.0.0 + */ +public class SyncServiceClientFactoryCxfRest extends SyncServiceClientFactoryCxf { + + private JacksonJsonProvider jsonProvider; + + /** + * The constructor. + */ + public SyncServiceClientFactoryCxfRest() { + super(); + } + + /** + * @param jsonProvider the {@link JacksonJsonProvider} to {@link Inject}. + */ + @Inject + public void setJsonProvider(JacksonJsonProvider jsonProvider) { + + this.jsonProvider = jsonProvider; + } + + @Override + protected void applyAspects(ServiceContext context, S serviceClient, String serviceName) { + + ClientConfiguration clientConfig = WebClient.getConfig(serviceClient); + applyInterceptors(context, clientConfig, serviceName); + applyClientPolicy(context, clientConfig.getHttpConduit()); + applyHeaders(context, serviceClient); + } + + @Override + protected S createService(ServiceContext context, String url, String serviceName) { + + List providers = createProviderList(context, serviceName); + return JAXRSClientFactory.create(url, context.getApi(), providers); + } + + @Override + protected String getServiceTypeFolderName() { + + return ServiceConstants.URL_FOLDER_REST; + } + + @Override + protected void applyHeaders(ServiceContext context, Object serviceClient) { + + Collection headerNames = context.getHeaderNames(); + if (!headerNames.isEmpty()) { + Client webClient = WebClient.client(serviceClient); + for (String headerName : headerNames) { + webClient.header(headerName, context.getHeader(headerName)); + } + } + } + + @Override + protected boolean isResponsibleForService(ServiceContext context) { + + return context.getApi().isAnnotationPresent(Path.class); + } + + /** + * @param context the {@link ServiceContext}. + * @param serviceName the {@link #createServiceName(ServiceContext) service name}. + * @return the {@link List} of {@link javax.ws.rs.ext.Provider}s. + */ + protected List createProviderList(ServiceContext context, String serviceName) { + + List providers = new ArrayList<>(); + if (this.jsonProvider != null) { + providers.add(this.jsonProvider); + } + providers.add(new RestServiceExceptionMapper(serviceName)); + return providers; + } + +} diff --git a/modules/cxf-client-ws/pom.xml b/modules/cxf-client-ws/pom.xml new file mode 100644 index 00000000..998966ee --- /dev/null +++ b/modules/cxf-client-ws/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-modules + dev-SNAPSHOT + + com.devonfw.java.modules + devon4j-cxf-client-ws + ${devon4j.version} + jar + ${project.artifactId} + Support for consuming web-service (SOAP) as client based on Apache CXF. + + + + ${project.groupId} + devon4j-cxf-client + + + org.apache.cxf + cxf-rt-frontend-jaxws + + + com.devonfw.java.modules + devon4j-test + test + + + + diff --git a/modules/cxf-client-ws/src/main/java/com/devonfw/module/cxf/common/impl/client/ws/CxfWsClientAutoConfiguration.java b/modules/cxf-client-ws/src/main/java/com/devonfw/module/cxf/common/impl/client/ws/CxfWsClientAutoConfiguration.java new file mode 100644 index 00000000..cc5dd4da --- /dev/null +++ b/modules/cxf-client-ws/src/main/java/com/devonfw/module/cxf/common/impl/client/ws/CxfWsClientAutoConfiguration.java @@ -0,0 +1,25 @@ +package com.devonfw.module.cxf.common.impl.client.ws; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.devonfw.module.service.common.api.sync.SyncServiceClientFactory; + +/** + * {@link Configuration} for SOAP (JAX-WS) clients using Apache CXF. + * + * @since 3.0.0 + */ +@Configuration +public class CxfWsClientAutoConfiguration { + + /** + * @return an implemenation of {@link SyncServiceClientFactory} based on CXF for SAOP (JAX-WS). + */ + @Bean + public SyncServiceClientFactory syncServiceClientFactoryCxfWs() { + + return new SyncServiceClientFactoryCxfWs(); + } + +} diff --git a/modules/cxf-client-ws/src/main/java/com/devonfw/module/cxf/common/impl/client/ws/SyncServiceClientFactoryCxfWs.java b/modules/cxf-client-ws/src/main/java/com/devonfw/module/cxf/common/impl/client/ws/SyncServiceClientFactoryCxfWs.java new file mode 100644 index 00000000..7e9b79ce --- /dev/null +++ b/modules/cxf-client-ws/src/main/java/com/devonfw/module/cxf/common/impl/client/ws/SyncServiceClientFactoryCxfWs.java @@ -0,0 +1,139 @@ +package com.devonfw.module.cxf.common.impl.client.ws; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.jws.WebService; +import javax.xml.namespace.QName; +import javax.xml.ws.BindingProvider; +import javax.xml.ws.Service; + +import org.apache.cxf.endpoint.Client; +import org.apache.cxf.frontend.ClientProxy; +import org.apache.cxf.message.Message; +import org.apache.cxf.transport.Conduit; +import org.apache.cxf.transport.http.HTTPConduit; + +import com.devonfw.module.cxf.common.impl.client.SyncServiceClientFactoryCxf; +import com.devonfw.module.service.common.api.client.context.ServiceContext; +import com.devonfw.module.service.common.api.config.ServiceConfig; +import com.devonfw.module.service.common.api.constants.ServiceConstants; +import com.devonfw.module.service.common.api.sync.SyncServiceClientFactory; + +/** + * Implementation of {@link SyncServiceClientFactory} for JAX-WS SOAP service clients using Apache CXF. + * + * @since 3.0.0 + */ +public class SyncServiceClientFactoryCxfWs extends SyncServiceClientFactoryCxf { + + private static final String WSDL_SUFFIX = "?wsdl"; + + @Override + protected S createService(ServiceContext context, String url, String serviceName) { + + Class api = context.getApi(); + WebService webService = api.getAnnotation(WebService.class); + QName qname = new QName(getNamespace(api, webService), getLocalName(api, webService)); + boolean downloadWsdl = context.getConfig().getChild(ServiceConfig.KEY_SEGMENT_WSDL) + .getChild(ServiceConfig.KEY_SEGMENT_DISABLE_DOWNLOAD).getValueAsBoolean(); + URL wsdlUrl = null; + if (downloadWsdl) { + try { + wsdlUrl = new URL(url); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Illegal URL: " + url, e); + } + } + S serviceClient = Service.create(wsdlUrl, qname).getPort(api); + if (!downloadWsdl) { + BindingProvider bindingProvider = (BindingProvider) serviceClient; + bindingProvider.getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, url); + } + return serviceClient; + } + + @Override + protected void applyAspects(ServiceContext context, S serviceClient, String serviceName) { + + Client cxfClient = ClientProxy.getClient(serviceClient); + applyInterceptors(context, cxfClient, serviceName); + Conduit conduit = cxfClient.getConduit(); + if (conduit instanceof HTTPConduit) { + HTTPConduit httpConduit = (HTTPConduit) conduit; + applyClientPolicy(context, httpConduit); + } + applyHeaders(context, cxfClient); + } + + @Override + protected String getServiceTypeFolderName() { + + return ServiceConstants.URL_FOLDER_WEB_SERVICE; + } + + @Override + protected String getUrl(ServiceContext context) { + + String url = super.getUrl(context); + if (!url.endsWith(WSDL_SUFFIX)) { + String serviceName = context.getApi().getSimpleName(); + if (!url.endsWith(serviceName)) { + if (!url.endsWith("/")) { + url = url + "/"; + } + url = url + serviceName; + } + url = url + WSDL_SUFFIX; + } + return url; + } + + private String getLocalName(Class api, WebService webService) { + + String portName = webService.portName(); + if (portName.isEmpty()) { + return api.getSimpleName(); + } + return portName; + } + + private String getNamespace(Class api, WebService webService) { + + String targetNamespace = webService.targetNamespace(); + if (targetNamespace.isEmpty()) { + return api.getPackage().getName(); + } + return targetNamespace; + } + + @Override + protected void applyHeaders(ServiceContext context, Object client) { + + Collection headerNames = context.getHeaderNames(); + if (!headerNames.isEmpty()) { + Map> headers = new HashMap<>(); + for (String headerName : headerNames) { + headers.put(headerName, Arrays.asList(context.getHeader(headerName))); + } + ((Client) client).getRequestContext().put(Message.PROTOCOL_HEADERS, headers); + } + } + + /** + * @param context the {@link ServiceContext}. + * @return {@code true} if this implementation is responsibe for creating a service client corresponding to the given + * {@link ServiceContext}, {@code false} otherwise. + */ + @Override + protected boolean isResponsibleForService(ServiceContext context) { + + return context.getApi().isAnnotationPresent(WebService.class); + } + +} diff --git a/modules/cxf-client/pom.xml b/modules/cxf-client/pom.xml new file mode 100644 index 00000000..77d8bbf7 --- /dev/null +++ b/modules/cxf-client/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-modules + dev-SNAPSHOT + + com.devonfw.java.modules + devon4j-cxf-client + ${devon4j.version} + jar + ${project.artifactId} + Basic support for consuming services as client based on Apache CXF. + + + + ${project.groupId} + devon4j-service + + + org.apache.cxf + cxf-rt-transports-http + + + org.springframework.boot + spring-boot + + + com.devonfw.java.modules + devon4j-test + test + + + + diff --git a/modules/cxf-client/src/main/java/com/devonfw/module/cxf/common/impl/client/CxfClientAutoConfiguration.java b/modules/cxf-client/src/main/java/com/devonfw/module/cxf/common/impl/client/CxfClientAutoConfiguration.java new file mode 100644 index 00000000..19afb259 --- /dev/null +++ b/modules/cxf-client/src/main/java/com/devonfw/module/cxf/common/impl/client/CxfClientAutoConfiguration.java @@ -0,0 +1,89 @@ +package com.devonfw.module.cxf.common.impl.client; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.devonfw.module.service.common.api.client.ServiceClientFactory; +import com.devonfw.module.service.common.api.client.discovery.ServiceDiscoverer; +import com.devonfw.module.service.common.api.config.ServiceConfig; +import com.devonfw.module.service.common.api.header.ServiceHeaderCustomizer; +import com.devonfw.module.service.common.base.config.ServiceConfigProperties; +import com.devonfw.module.service.common.impl.ServiceClientFactoryImpl; +import com.devonfw.module.service.common.impl.discovery.ServiceDiscovererImplConfig; +import com.devonfw.module.service.common.impl.header.ServiceHeaderCustomizerAuthForward; +import com.devonfw.module.service.common.impl.header.ServiceHeaderCustomizerBasicAuth; +import com.devonfw.module.service.common.impl.header.ServiceHeaderCustomizerCorrelationId; +import com.devonfw.module.service.common.impl.header.ServiceHeaderCustomizerOAuth; + +/** + * {@link Configuration} for REST (JAX-RS) clients using Apache CXF. + * + * @since 3.0.0 + */ +@Configuration +// @Import(ServiceClientSpringFactory.class) +public class CxfClientAutoConfiguration { + + /** + * @return the implementation of {@link ServiceClientFactory}. + */ + @Bean + public ServiceClientFactory serviceClientFactory() { + + return new ServiceClientFactoryImpl(); + } + + /** + * @return the implementation of {@link ServiceConfig}. + */ + @Bean + public ServiceConfig serviceClientConfig() { + + return new ServiceConfigProperties(); + } + + /** + * @return an implementation of {@link ServiceDiscoverer} based on {@link #serviceClientConfig()}. + */ + @Bean + public ServiceDiscoverer serviceDiscovererConfig() { + + return new ServiceDiscovererImplConfig(); + } + + /** + * @return an implementation of {@link ServiceHeaderCustomizer} passing correlation ID. + */ + @Bean + public ServiceHeaderCustomizer serviceHeaderCustomizerCorrelationId() { + + return new ServiceHeaderCustomizerCorrelationId(); + } + + /** + * @return an implementation of {@link ServiceHeaderCustomizer} for basic authentication support. + */ + @Bean + public ServiceHeaderCustomizer serviceHeaderCustomizerBasicAuth() { + + return new ServiceHeaderCustomizerBasicAuth(); + } + + /** + * @return an implementation of {@link ServiceHeaderCustomizer} for OAuth support. + */ + @Bean + public ServiceHeaderCustomizer serviceHeaderCustomizerOAuth() { + + return new ServiceHeaderCustomizerOAuth(); + } + + /** + * @return an implementation of {@link ServiceHeaderCustomizer} for JWT support. + */ + @Bean + public ServiceHeaderCustomizer serviceHeaderCustomizerAuthForward() { + + return new ServiceHeaderCustomizerAuthForward(); + } +} diff --git a/modules/cxf-client/src/main/java/com/devonfw/module/cxf/common/impl/client/SyncServiceClientFactoryCxf.java b/modules/cxf-client/src/main/java/com/devonfw/module/cxf/common/impl/client/SyncServiceClientFactoryCxf.java new file mode 100644 index 00000000..fb517e98 --- /dev/null +++ b/modules/cxf-client/src/main/java/com/devonfw/module/cxf/common/impl/client/SyncServiceClientFactoryCxf.java @@ -0,0 +1,157 @@ +package com.devonfw.module.cxf.common.impl.client; + +import org.apache.cxf.interceptor.InterceptorProvider; +import org.apache.cxf.transport.http.HTTPConduit; +import org.apache.cxf.transports.http.configuration.HTTPClientPolicy; + +import com.devonfw.module.basic.common.api.config.ConfigProperties; +import com.devonfw.module.cxf.common.impl.client.interceptor.PerformanceStartInterceptor; +import com.devonfw.module.cxf.common.impl.client.interceptor.PerformanceStopInterceptor; +import com.devonfw.module.cxf.common.impl.client.interceptor.TechnicalExceptionInterceptor; +import com.devonfw.module.service.common.api.client.context.ServiceContext; +import com.devonfw.module.service.common.api.config.ServiceConfig; +import com.devonfw.module.service.common.api.constants.ServiceConstants; +import com.devonfw.module.service.common.api.sync.SyncServiceClientFactory; + +/** + * Abstract base implementation of {@link SyncServiceClientFactory} for service clients using Apache CXF. + * + * @since 3.0.0 + */ +public abstract class SyncServiceClientFactoryCxf implements SyncServiceClientFactory { + + @Override + public S create(ServiceContext context) { + + boolean responsible = isResponsibleForService(context); + if (!responsible) { + return null; + } + String serviceName = createServiceName(context); + + String url = getUrl(context); + S serviceClient = createService(context, url, serviceName); + + applyAspects(context, serviceClient, serviceName); + return serviceClient; + } + + /** + * @param the generic type of the {@link ServiceContext#getApi() service API}. + * @param context the {@link ServiceContext}. + * @param url the resolved end-point URL of the service to invoke. + * @param serviceName the {@link #createServiceName(ServiceContext) service name}. + * @return a new client stub for the service. See {@link #create(ServiceContext)} for further details. + */ + protected abstract S createService(ServiceContext context, String url, String serviceName); + + /** + * @param context the {@link ServiceContext}. + * @return the {@link ServiceContext#getUrl() URL} with additional post-processing such as resolving variables. + */ + protected String getUrl(ServiceContext context) { + + String url = context.getUrl(); + url = url.replace(ServiceConstants.VARIABLE_TYPE, getServiceTypeFolderName()); + return url; + } + + /** + * Implementations should call the following methods: + *
    + *
  • {@link #applyInterceptors(ServiceContext, InterceptorProvider, String)}
  • + *
  • {@link #applyClientPolicy(ServiceContext, HTTPConduit)}
  • + *
  • {@link #applyHeaders(ServiceContext, Object)}
  • + *
+ * + * @param the generic type of the {@link ServiceContext#getApi() service API}. + * @param context the {@link ServiceContext}. + * @param serviceClient the {@link #create(ServiceContext) created service client stub}. + * @param serviceName the {@link #createServiceName(ServiceContext) service name}. + */ + protected abstract void applyAspects(ServiceContext context, S serviceClient, String serviceName); + + /** + * @return the resolved value for {@link ServiceConstants#VARIABLE_TYPE} such as e.g. + * {@link ServiceConstants#URL_FOLDER_REST}. + */ + protected abstract String getServiceTypeFolderName(); + + /** + * @param context the {@link ServiceContext}. + * @return the {@link HTTPClientPolicy} for the {@link ServiceContext#getConfig() configuration} or {@code null} to + * use defaults. + */ + protected HTTPClientPolicy createClientPolicy(ServiceContext context) { + + ConfigProperties timeoutConfig = context.getConfig().getChild(ServiceConfig.KEY_SEGMENT_TIMEOUT); + if (!timeoutConfig.isEmpty()) { + HTTPClientPolicy policy = new HTTPClientPolicy(); + Long connectionTimeout = + timeoutConfig.getChild(ServiceConfig.KEY_SEGMENT_TIMEOUT_CONNECTION).getValue(Long.class); + if (connectionTimeout != null) { + policy.setConnectionTimeout(connectionTimeout.longValue()); + } + Long responseTimeout = timeoutConfig.getChild(ServiceConfig.KEY_SEGMENT_TIMEOUT_RESPONSE).getValue(Long.class); + if (responseTimeout != null) { + policy.setReceiveTimeout(responseTimeout.longValue()); + } + return policy; + } + return null; + } + + /** + * @param context the {@link ServiceContext}. + * @return the display name of the service for exception or log messages. + */ + protected String createServiceName(ServiceContext context) { + + return context.getApi().getName(); + } + + /** + * Applies CXF interceptors to the given {@code serviceClient}. + * + * @param context the {@link ServiceContext}. + * @param client the {@link InterceptorProvider}. + * @param serviceName the {@link #createServiceName(ServiceContext) service name}. + */ + protected void applyInterceptors(ServiceContext context, InterceptorProvider client, String serviceName) { + + client.getOutInterceptors().add(new PerformanceStartInterceptor()); + client.getInInterceptors().add(new PerformanceStopInterceptor()); + client.getInFaultInterceptors().add(new TechnicalExceptionInterceptor(serviceName)); + } + + /** + * @param context the {@link ServiceContext}. + * @param conduit the {@link HTTPConduit} where to apply the {@link HTTPClientPolicy} to. + */ + protected void applyClientPolicy(ServiceContext context, HTTPConduit conduit) { + + if (conduit == null) { + return; + } + HTTPClientPolicy clientPolicy = createClientPolicy(context); + if (clientPolicy != null) { + conduit.setClient(clientPolicy); + } + } + + /** + * Applies headers to the given {@code serviceClient}. + * + * @param context the {@link ServiceContext}. + * @param serviceClient the service client instance. + */ + protected abstract void applyHeaders(ServiceContext context, Object serviceClient); + + /** + * @param context the {@link ServiceContext}. + * @return {@code true} if this implementation is responsibe for creating a service client corresponding to the given + * {@link ServiceContext}, {@code false} otherwise. + */ + protected abstract boolean isResponsibleForService(ServiceContext context); + +} diff --git a/modules/cxf-client/src/main/java/com/devonfw/module/cxf/common/impl/client/interceptor/PerformanceStartInterceptor.java b/modules/cxf-client/src/main/java/com/devonfw/module/cxf/common/impl/client/interceptor/PerformanceStartInterceptor.java new file mode 100644 index 00000000..abaaf8ba --- /dev/null +++ b/modules/cxf-client/src/main/java/com/devonfw/module/cxf/common/impl/client/interceptor/PerformanceStartInterceptor.java @@ -0,0 +1,30 @@ +package com.devonfw.module.cxf.common.impl.client.interceptor; + +import net.sf.mmm.util.date.api.TimeMeasure; + +import org.apache.cxf.interceptor.Fault; +import org.apache.cxf.message.Message; +import org.apache.cxf.phase.AbstractPhaseInterceptor; +import org.apache.cxf.phase.Phase; + +/** + * Implementation of {@link AbstractPhaseInterceptor} that logs the duration time of a service client invocation. + * + * @since 3.0.0 + */ +public class PerformanceStartInterceptor extends AbstractPhaseInterceptor { + + /** + * The constructor. + */ + public PerformanceStartInterceptor() { + super(Phase.SETUP); + } + + @Override + public void handleMessage(Message message) throws Fault { + + message.getExchange().put(TimeMeasure.class, new TimeMeasure()); + } + +} diff --git a/modules/cxf-client/src/main/java/com/devonfw/module/cxf/common/impl/client/interceptor/PerformanceStopInterceptor.java b/modules/cxf-client/src/main/java/com/devonfw/module/cxf/common/impl/client/interceptor/PerformanceStopInterceptor.java new file mode 100644 index 00000000..b8832cb5 --- /dev/null +++ b/modules/cxf-client/src/main/java/com/devonfw/module/cxf/common/impl/client/interceptor/PerformanceStopInterceptor.java @@ -0,0 +1,53 @@ +package com.devonfw.module.cxf.common.impl.client.interceptor; + +import net.sf.mmm.util.date.api.TimeMeasure; + +import org.apache.cxf.interceptor.Fault; +import org.apache.cxf.message.Message; +import org.apache.cxf.phase.AbstractPhaseInterceptor; +import org.apache.cxf.phase.Phase; +import org.apache.cxf.transport.Conduit; +import org.apache.cxf.ws.addressing.EndpointReferenceType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implementation of {@link AbstractPhaseInterceptor} that logs the duration time of a service client invocation. + * + * @since 3.0.0 + */ +public class PerformanceStopInterceptor extends AbstractPhaseInterceptor { + + private static final Logger LOG = LoggerFactory.getLogger(PerformanceStopInterceptor.class); + + private boolean errorLogged; + + /** + * The constructor. + */ + public PerformanceStopInterceptor() { + super(Phase.POST_INVOKE); + } + + @Override + public void handleMessage(Message message) throws Fault { + + TimeMeasure timeMeasure = message.getExchange().get(TimeMeasure.class); + if (timeMeasure == null) { + if (!this.errorLogged) { + LOG.warn("Invalid setup - no TimeMeasure present!"); + this.errorLogged = true; + } + return; + } + Throwable exception = message.getContent(Exception.class); + if (exception == null) { + timeMeasure.succeed(); + } + Conduit conduit = message.get(Conduit.class); + EndpointReferenceType target = conduit.getTarget(); + String url = target.getAddress().getValue(); + timeMeasure.log(LOG, url); + } + +} diff --git a/modules/cxf-client/src/main/java/com/devonfw/module/cxf/common/impl/client/interceptor/TechnicalExceptionInterceptor.java b/modules/cxf-client/src/main/java/com/devonfw/module/cxf/common/impl/client/interceptor/TechnicalExceptionInterceptor.java new file mode 100644 index 00000000..ed059206 --- /dev/null +++ b/modules/cxf-client/src/main/java/com/devonfw/module/cxf/common/impl/client/interceptor/TechnicalExceptionInterceptor.java @@ -0,0 +1,42 @@ +package com.devonfw.module.cxf.common.impl.client.interceptor; + +import net.sf.mmm.util.exception.api.ServiceInvocationFailedException; + +import org.apache.cxf.interceptor.Fault; +import org.apache.cxf.message.Message; +import org.apache.cxf.phase.AbstractPhaseInterceptor; +import org.apache.cxf.phase.Phase;; + +/** + * Implementation of {@link AbstractPhaseInterceptor} to handle technical errors like {@link java.net.ConnectException} + * or {@link java.net.SocketTimeoutException}. + * + * @since 3.0.0 + */ +public class TechnicalExceptionInterceptor extends AbstractPhaseInterceptor { + + private final String service; + + /** + * The constructor. + * + * @param service the name (e.g. {@link Class#getName() qualified name}) of the + * {@link com.devonfw.module.service.common.api.Service} that failed. + */ + public TechnicalExceptionInterceptor(String service) { + + super(Phase.PRE_PROTOCOL); + this.service = service; + } + + @Override + public void handleMessage(Message message) throws Fault { + + Throwable exception = message.getContent(Exception.class); + if (exception != null) { + message.getExchange().put("wrap.in.processing.exception", Boolean.FALSE); + throw new ServiceInvocationFailedException(exception, exception.toString(), "ServiceInvoke", null, this.service); + } + } + +} diff --git a/modules/cxf-server-rest/pom.xml b/modules/cxf-server-rest/pom.xml new file mode 100644 index 00000000..5e69c57c --- /dev/null +++ b/modules/cxf-server-rest/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-modules + dev-SNAPSHOT + + com.devonfw.java.modules + devon4j-cxf-server-rest + ${devon4j.version} + jar + ${project.artifactId} + Support for providing REST services on as server based on Apache CXF. + + + + ${project.groupId} + devon4j-cxf-server + + + ${project.groupId} + devon4j-rest + + + org.apache.cxf + cxf-rt-frontend-jaxrs + + + com.devonfw.java.modules + devon4j-test + test + + + + diff --git a/modules/cxf-server-rest/src/main/java/com/devonfw/module/cxf/common/impl/server/rest/CxfServerRestAutoConfiguration.java b/modules/cxf-server-rest/src/main/java/com/devonfw/module/cxf/common/impl/server/rest/CxfServerRestAutoConfiguration.java new file mode 100644 index 00000000..e1ba1b74 --- /dev/null +++ b/modules/cxf-server-rest/src/main/java/com/devonfw/module/cxf/common/impl/server/rest/CxfServerRestAutoConfiguration.java @@ -0,0 +1,119 @@ +package com.devonfw.module.cxf.common.impl.server.rest; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; +import javax.ws.rs.ext.Provider; + +import org.apache.cxf.bus.spring.SpringBus; +import org.apache.cxf.endpoint.Server; +import org.apache.cxf.jaxrs.JAXRSServerFactoryBean; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.devonfw.module.json.common.base.ObjectMapperFactory; +import com.devonfw.module.rest.common.api.RestService; +import com.devonfw.module.rest.service.impl.RestServiceExceptionFacade; +import com.devonfw.module.service.common.api.constants.ServiceConstants; +import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider; + +/** + * {@link Configuration} for REST (JAX-RS) services server using Apache CXF. + * + * @since 3.0.0 + */ +@Configuration +public class CxfServerRestAutoConfiguration { + + /** Logger instance. */ + private static final Logger LOG = LoggerFactory.getLogger(CxfServerRestAutoConfiguration.class); + + @Value("${security.expose.error.details:false}") + private boolean exposeInternalErrorDetails; + + @Value("${service.rest.find-by-name:true}") + private boolean findRestServicesByName; + + @Inject + private ConfigurableApplicationContext applicationContext; + + @Inject + private SpringBus springBus; + + @Inject + private ObjectMapperFactory objectMapperFactory; + + /** + * @return the {@link Server} that provides the REST (JAX-RS) services. + */ + @Bean + public Server jaxRsServer() { + + List restServices = findRestServices(); + if (restServices.isEmpty()) { + LOG.info("No REST Services have been found. Rest Endpoint will not be enabled in CXF."); + return null; + } + List providers = findRestProviders(); + + JAXRSServerFactoryBean factory = new JAXRSServerFactoryBean(); + factory.setBus(this.springBus); + factory.setAddress("/" + ServiceConstants.URL_FOLDER_REST); + factory.setServiceBeans(restServices); + factory.setProviders(providers); + return factory.create(); + } + + /** + * @return the {@link JacksonJsonProvider} for JSON support in REST (JAX-RS) services. + */ + @Bean + public JacksonJsonProvider jacksonJsonProvider() { + + return new JacksonJsonProvider(this.objectMapperFactory.createInstance()); + } + + /** + * @return the {@link RestServiceExceptionFacade} used to handle {@link Exception}s during REST calls on the + * server-side and map them to according status code and JSON result. + */ + @Bean + public RestServiceExceptionFacade restServiceExceptionFacade() { + + RestServiceExceptionFacade exceptionFacade = new RestServiceExceptionFacade(); + exceptionFacade.setExposeInternalErrorDetails(this.exposeInternalErrorDetails); + return exceptionFacade; + } + + /** + * @return the {@link List} of REST (JAX-RS) {@link Provider}s. + */ + protected List findRestProviders() { + + return new ArrayList<>(this.applicationContext.getBeansWithAnnotation(Provider.class).values()); + } + + /** + * @return the {@link List} of {@link RestService}s. + */ + protected List findRestServices() { + + if (this.findRestServicesByName) { + List result = new ArrayList<>(); + for (String beanName : this.applicationContext.getBeanDefinitionNames()) { + if (beanName.contains("RestService")) { + result.add(this.applicationContext.getBean(beanName)); + } + } + return result; + } else { + return new ArrayList(this.applicationContext.getBeansOfType(RestService.class).values()); + } + } + +} \ No newline at end of file diff --git a/modules/cxf-server-ws/pom.xml b/modules/cxf-server-ws/pom.xml new file mode 100644 index 00000000..22a6f73f --- /dev/null +++ b/modules/cxf-server-ws/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-modules + dev-SNAPSHOT + + com.devonfw.java.modules + devon4j-cxf-server-ws + ${devon4j.version} + jar + ${project.artifactId} + Support for providing web-services (SOAP) as server based on Apache CXF. + + + + ${project.groupId} + devon4j-cxf-server + + + org.apache.cxf + cxf-rt-frontend-jaxws + + + org.springframework.boot + spring-boot-starter-web-services + + + com.devonfw.java.modules + devon4j-test + test + + + + diff --git a/modules/cxf-server-ws/src/main/java/com/devonfw/module/cxf/common/impl/server/soap/CxfServerSoapAutoConfiguration.java b/modules/cxf-server-ws/src/main/java/com/devonfw/module/cxf/common/impl/server/soap/CxfServerSoapAutoConfiguration.java new file mode 100644 index 00000000..e258d2ae --- /dev/null +++ b/modules/cxf-server-ws/src/main/java/com/devonfw/module/cxf/common/impl/server/soap/CxfServerSoapAutoConfiguration.java @@ -0,0 +1,72 @@ +package com.devonfw.module.cxf.common.impl.server.soap; + +import java.util.Map; +import java.util.Map.Entry; + +import javax.inject.Inject; +import javax.jws.WebService; + +import org.apache.cxf.bus.spring.SpringBus; +import org.apache.cxf.jaxws.EndpointImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.ws.config.annotation.EnableWs; +import org.springframework.ws.config.annotation.WsConfigurerAdapter; + +import com.devonfw.module.service.common.api.constants.ServiceConstants; + +/** + * {@link Configuration} for (REST or SOAP) services server using CXF.
+ * + * Scans for spring beans that represent a SOAP service (JAX-WS web service) by checking for {@link WebService} + * annotation. It will register all these {@link WebService}s to CXF using their spring bean name as URL path. Hence you + * should annotate your {@link WebService} implementation using {@link javax.inject.Named} providing a reasonable name: + * + *
+ * @{@link javax.inject.Named}("MyWebService")
+ * @{@link WebService}(endpointInterface = "my.package.MyWebService")
+ * public class MyWebServiceImpl implements MyWebService {
+ *   ...
+ * }
+ * 
+ * + * @since 3.0.0 + */ +@Configuration +@EnableWs +public class CxfServerSoapAutoConfiguration extends WsConfigurerAdapter { + + /** Logger instance. */ + private static final Logger LOG = LoggerFactory.getLogger(CxfServerSoapAutoConfiguration.class); + + @Inject + private SpringBus springBus; + + @Inject + private ConfigurableApplicationContext applicationContext; + + /** + * @see CxfServerSoapAutoConfiguration + * @return will always return {@code null}. + */ + @Bean + public Object registerWebServices() { + + Map webServiceMap = this.applicationContext.getBeansWithAnnotation(WebService.class); + if (webServiceMap.isEmpty()) { + LOG.info("No SOAP Services have been found. SOAP Endpoint will not be enabled in CXF."); + return null; + } + ConfigurableListableBeanFactory beanFactory = this.applicationContext.getBeanFactory(); + for (Entry serviceEntry : webServiceMap.entrySet()) { + EndpointImpl endpoint = new org.apache.cxf.jaxws.EndpointImpl(this.springBus, serviceEntry.getValue()); + endpoint.publish("/" + ServiceConstants.URL_FOLDER_WEB_SERVICE + "/" + serviceEntry.getKey()); + beanFactory.registerSingleton(serviceEntry.getKey() + "Endpoint", endpoint); + } + return null; + } +} \ No newline at end of file diff --git a/modules/cxf-server/pom.xml b/modules/cxf-server/pom.xml new file mode 100644 index 00000000..2334e948 --- /dev/null +++ b/modules/cxf-server/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-modules + dev-SNAPSHOT + + com.devonfw.java.modules + devon4j-cxf-server + ${devon4j.version} + jar + ${project.artifactId} + Basic support for providing services as server based on Apache CXF. + + + + ${project.groupId} + devon4j-service + + + org.apache.cxf + cxf-rt-transports-http + + + org.springframework.boot + spring-boot + + + javax.servlet + javax.servlet-api + provided + + + com.devonfw.java.modules + devon4j-test + test + + + + diff --git a/modules/cxf-server/src/main/java/com/devonfw/module/cxf/common/impl/server/CxfServerAutoConfiguration.java b/modules/cxf-server/src/main/java/com/devonfw/module/cxf/common/impl/server/CxfServerAutoConfiguration.java new file mode 100644 index 00000000..5d80843e --- /dev/null +++ b/modules/cxf-server/src/main/java/com/devonfw/module/cxf/common/impl/server/CxfServerAutoConfiguration.java @@ -0,0 +1,33 @@ +package com.devonfw.module.cxf.common.impl.server; + +import org.apache.cxf.transport.servlet.CXFServlet; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportResource; + +import com.devonfw.module.service.common.api.constants.ServiceConstants; + +/** + * Basic {@link Configuration} for Apache CXF server. + * + * @since 3.0.0 + */ +@Configuration +@ImportResource({ "classpath:META-INF/cxf/cxf.xml" }) +public class CxfServerAutoConfiguration { + + /** + * @return the {@link ServletRegistrationBean} to register {@link CXFServlet} according to Devon4j conventions at + * {@link ServiceConstants#URL_PATH_SERVICES}. + */ + @Bean + public ServletRegistrationBean servletRegistrationBean() { + + CXFServlet cxfServlet = new CXFServlet(); + ServletRegistrationBean servletRegistration = new ServletRegistrationBean(cxfServlet, + ServiceConstants.URL_PATH_SERVICES + "/*"); + return servletRegistration; + } + +} \ No newline at end of file diff --git a/modules/jpa-basic/pom.xml b/modules/jpa-basic/pom.xml new file mode 100644 index 00000000..84e213cd --- /dev/null +++ b/modules/jpa-basic/pom.xml @@ -0,0 +1,95 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-modules + dev-SNAPSHOT + + com.devonfw.java.modules + devon4j-jpa-basic + ${devon4j.version} + jar + ${project.artifactId} + JPA-based persistence infrastructure of the Open Application Standard Platform for Java (devon4j). + + + + com.devonfw.java.modules + devon4j-basic + + + + org.hibernate.javax.persistence + hibernate-jpa-2.1-api + + + javax.transaction + javax.transaction-api + + + org.springframework.data + spring-data-commons + + + com.querydsl + querydsl-jpa + + + cglib + cglib + + + org.hibernate + hibernate-entitymanager + true + + + org.hibernate + hibernate-envers + true + + + org.springframework + spring-beans + + + net.sf.m-m-m + mmm-util-search + + + + com.devonfw.java.modules + devon4j-test + test + + + com.devonfw.java.modules + devon4j-configuration + test + + + org.springframework + spring-orm + test + + + org.springframework + spring-context + test + + + com.h2database + h2 + test + + + + + \ No newline at end of file diff --git a/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/AdvancedRevisionEntity.java b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/AdvancedRevisionEntity.java new file mode 100644 index 00000000..6549eaa6 --- /dev/null +++ b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/AdvancedRevisionEntity.java @@ -0,0 +1,128 @@ +/* Copyright (c) The m-m-m Team, Licensed under the Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0 */ +package com.devonfw.module.jpa.dataaccess.api; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import net.sf.mmm.util.entity.api.PersistenceEntity; + +import org.hibernate.envers.RevisionEntity; +import org.hibernate.envers.RevisionNumber; +import org.hibernate.envers.RevisionTimestamp; + +/** + * This is a custom {@link org.hibernate.envers.DefaultRevisionEntity revision entity} also containing the actual user. + * + * @see org.hibernate.envers.DefaultRevisionEntity + */ +@Entity +@RevisionEntity(AdvancedRevisionListener.class) +@Table(name = "RevInfo") +public class AdvancedRevisionEntity implements PersistenceEntity { + + /** UID for serialization. */ + private static final long serialVersionUID = 1L; + + /** @see #getId() */ + @Id + @GeneratedValue + @RevisionNumber + private Long id; + + /** @see #getTimestamp() */ + @RevisionTimestamp + @Column(name = "\"timestamp\"") + private long timestamp; + + /** @see #getDate() */ + private transient Date date; + + /** @see #getUserLogin() */ + + private String userLogin; + + /** + * The constructor. + */ + public AdvancedRevisionEntity() { + + super(); + } + + @Override + public Long getId() { + + return this.id; + } + + /** + * @param id is the new value of {@link #getId()}. + */ + public void setId(Long id) { + + this.id = id; + } + + /** + * @return the timestamp when this revision has been created. + */ + public long getTimestamp() { + + return this.timestamp; + } + + /** + * @return the {@link #getTimestamp() timestamp} as {@link Date}. + */ + public Date getDate() { + + if (this.date == null) { + this.date = new Date(this.timestamp); + } + return this.date; + } + + /** + * @param timestamp is the new value of {@link #getTimestamp()}. + */ + public void setTimestamp(long timestamp) { + + this.timestamp = timestamp; + } + + /** + * @return the login or id of the user that has created this revision. + */ + + public String getUserLogin() { + + return this.userLogin; + } + + /** + * @param userLogin is the new value of {@link #getUserLogin()}. + */ + public void setUserLogin(String userLogin) { + + this.userLogin = userLogin; + } + + @Override + public int getModificationCounter() { + + return 0; + } + + @Override + public Number getRevision() { + + return null; + } +} diff --git a/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/AdvancedRevisionListener.java b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/AdvancedRevisionListener.java new file mode 100644 index 00000000..b8ea67e5 --- /dev/null +++ b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/AdvancedRevisionListener.java @@ -0,0 +1,30 @@ +/* Copyright (c) The m-m-m Team, Licensed under the Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0 */ +package com.devonfw.module.jpa.dataaccess.api; + +import net.sf.mmm.util.session.api.UserSessionAccess; + +import org.hibernate.envers.RevisionListener; + +/** + * This is the implementation of {@link RevisionListener} that enriches {@link AdvancedRevisionEntity} with additional + * information. + */ +public class AdvancedRevisionListener implements RevisionListener { + + /** + * The constructor. + */ + public AdvancedRevisionListener() { + + super(); + } + + @Override + public void newRevision(Object revisionEntity) { + + AdvancedRevisionEntity revision = (AdvancedRevisionEntity) revisionEntity; + revision.setUserLogin(UserSessionAccess.getUserLogin()); + } + +} \ No newline at end of file diff --git a/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/DatabaseConfigProperties.java b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/DatabaseConfigProperties.java new file mode 100644 index 00000000..cc05bc7a --- /dev/null +++ b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/DatabaseConfigProperties.java @@ -0,0 +1,70 @@ +package com.devonfw.module.jpa.dataaccess.api; + +import javax.annotation.PostConstruct; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; + +/** + * Workaround if forced to allow more IN-clause values than supported by your database if really required. Please use + * this feature with extreme caution. Databases do have their reasons for such limitations. Using to many values might + * also kill your statement cache in the database. + * + * @since 3.0.0 + */ +class DatabaseConfigProperties { + + private static final Logger LOG = LoggerFactory.getLogger(DatabaseConfigProperties.class); + + private static DatabaseConfigProperties instance; + + @Value("${database.query.in-clause.max-values:2147483647}") + private int maxSizeOfInClause; + + /** Die maximale Anzahl von Werten, welche Oracle in einer IN-Klausel verarbeiten kann. */ + public static final int MAX_SIZE_OF_IN_CLAUSE_IN_ORACLE = 1000; + + /** + * The constructor. + */ + public DatabaseConfigProperties() { + + super(); + this.maxSizeOfInClause = Integer.MAX_VALUE; + } + + @PostConstruct + protected void initialize() { + + if (instance == null) { + instance = this; + LOG.debug("Registering DB configuration instance {}.", this); + } else { + LOG.warn("Instance {} has already been registered and can not be replaced by {}", instance, this); + } + } + + public int getMaxSizeOfInClause() { + + return this.maxSizeOfInClause; + } + + /** + * @return instance + */ + public static DatabaseConfigProperties getInstance() { + + if (instance == null) { + new DatabaseConfigProperties(); + } + return instance; + } + + @Override + public String toString() { + + return getClass().getSimpleName() + "[maxSizeOfInClause=" + this.maxSizeOfInClause + "]"; + } + +} diff --git a/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/JpaEntityManagerAccess.java b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/JpaEntityManagerAccess.java new file mode 100644 index 00000000..0a06972f --- /dev/null +++ b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/JpaEntityManagerAccess.java @@ -0,0 +1,43 @@ +package com.devonfw.module.jpa.dataaccess.api; + +import javax.persistence.EntityManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Internal access to {@link EntityManager}. + */ +class JpaEntityManagerAccess { + + private static final Logger LOG = LoggerFactory.getLogger(JpaEntityManagerAccess.class); + + private static EntityManager entityManager; + + static void setEntityManager(EntityManager entityManager, boolean check) { + + if ((JpaEntityManagerAccess.entityManager != null) && (JpaEntityManagerAccess.entityManager != entityManager)) { + if (check) { + throw new IllegalStateException("EntityManager has already been initialized!"); + } else { + LOG.debug("EntityManager conflict: {} has been replaced with {}. This may only happen during tests.", + JpaEntityManagerAccess.entityManager, entityManager); + } + } + JpaEntityManagerAccess.entityManager = entityManager; + } + + static boolean hasEntityManager() { + + return (entityManager != null); + } + + static EntityManager getEntityManager() { + + if (entityManager == null) { + throw new IllegalStateException("EntityManager has not yet been initialized!"); + } + return entityManager; + } + +} diff --git a/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/JpaHelper.java b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/JpaHelper.java new file mode 100644 index 00000000..6c586f9e --- /dev/null +++ b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/JpaHelper.java @@ -0,0 +1,78 @@ +package com.devonfw.module.jpa.dataaccess.api; + +import java.util.Collection; + +import net.sf.mmm.util.entity.api.GenericEntity; + +import com.devonfw.module.basic.common.api.reference.GenericIdRef; +import com.devonfw.module.basic.common.api.reference.Ref; + +/** + * Helper class for generic handling of {@link net.sf.mmm.util.entity.api.PersistenceEntity persistence entities} (based + * on {@link javax.persistence.EntityManager}). In some cases it is required to access JPA features in a static way. + * E.g. a common case is a setter in your {@link net.sf.mmm.util.entity.api.PersistenceEntity} for a + * {@link com.devonfw.module.basic.common.api.reference.Ref reference} from an + * {@link com.devonfw.module.basic.common.api.to.AbstractEto ETO} that can be archieved via the following code: + * + *
+ * @Entity
+ * @Table("Foo")
+ * public class FooEntity extends ApplicationPersistenceEntity implements Foo {
+ *   @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
+ *   @JoinColumn(name = "bar")
+ *   private BarEntity bar;
+ *   ...
+ *   @Override
+ *   public void setBarId({@link com.devonfw.module.basic.common.api.reference.IdRef}{@literal } barId) {
+ *     this.bar = {@link JpaHelper}.{@link JpaHelper#asEntity(Ref, Class) asEntity}(barId, BarEntity.class);
+ *   }
+ * }
+ * 
+ */ +public class JpaHelper { + + /** + * @param generic type of the {@link net.sf.mmm.util.entity.api.PersistenceEntity entity} + * @param reference the {@link Ref} or {@code null}. Typically an + * {@link com.devonfw.module.basic.common.api.reference.IdRef}. + * @param entityClass the {@link net.sf.mmm.util.entity.api.PersistenceEntity entity} {@link Class}. + * @return the {@link net.sf.mmm.util.entity.api.PersistenceEntity entity} of the specified {@link Class} with the + * {@link Ref#getId() ID} from the given {@link GenericIdRef} or {@code null} if the given {@link Ref} is + * {@code null}. + */ + public static E asEntity(Ref reference, Class entityClass) { + + if (reference == null) { + return null; + } else { + return JpaEntityManagerAccess.getEntityManager().getReference(entityClass, reference.getId()); + } + } + + /** + * @param generic type of the input {@link GenericEntity entities} (most commonly the entity interface). + * @param

generic type of the output {@link net.sf.mmm.util.entity.api.PersistenceEntity persistence entities}. + * @param input the {@link Collection} of {@link GenericEntity entities} (e.g. + * {@link com.devonfw.module.basic.common.api.to.AbstractEto ETOs}) to use as input. + * @param entityClass the {@link Class} reflecting the {@link net.sf.mmm.util.entity.api.PersistenceEntity}. + * @param output die {@link Collection} where to {@link Collection#add(Object) add} the + * {@link net.sf.mmm.util.entity.api.PersistenceEntity persistent entities} corresponding to the input + * {@link GenericEntity entities}. Most probably {@link Collection#isEmpty() empty} but may also already + * contain entities so this method will add additional entities. + */ + @SuppressWarnings("unchecked") + public static , P extends E> void asEntities(Collection input, + Class

entityClass, Collection

output) { + + for (E eto : input) { + P entity; + if (entityClass.isInstance(eto)) { + entity = (P) eto; + } else { + entity = JpaEntityManagerAccess.getEntityManager().getReference(entityClass, eto.getId()); + } + output.add(entity); + } + } + +} diff --git a/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/JpaInitializer.java b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/JpaInitializer.java new file mode 100644 index 00000000..545b9463 --- /dev/null +++ b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/JpaInitializer.java @@ -0,0 +1,29 @@ +package com.devonfw.module.jpa.dataaccess.api; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +/** + * Initializer bean for {@link EntityManager}. Will be auto configured via {@code devon4j-starter-jpa}. + */ +public class JpaInitializer { + + /** + * @param entityManager the {@link EntityManager} to inject. + */ + @PersistenceContext + protected void setEntityManager(EntityManager entityManager) { + + JpaEntityManagerAccess.setEntityManager(entityManager, true); + } + + /** + * @param entityManager the {@link EntityManager} to set. + * @param check - {@code true} to check that the {@link EntityManager} does not change on-the-fly (desired in + * productive code), {@code false} otherwise (may be desired in test-code). + */ + protected void setEntityManager(EntityManager entityManager, boolean check) { + + JpaEntityManagerAccess.setEntityManager(entityManager, check); + } +} diff --git a/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/MutablePersistenceEntity.java b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/MutablePersistenceEntity.java new file mode 100644 index 00000000..5e955169 --- /dev/null +++ b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/MutablePersistenceEntity.java @@ -0,0 +1,18 @@ +/* Copyright (c) The m-m-m Team, Licensed under the Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0 */ +package com.devonfw.module.jpa.dataaccess.api; + +import net.sf.mmm.util.entity.api.MutableRevisionedEntity; +import net.sf.mmm.util.entity.api.PersistenceEntity; + +/** + * This is the interface for a {@link PersistenceEntity} in Devon4j. + * + * @param is the type of the {@link #getId() primary key}. + */ +public interface MutablePersistenceEntity extends PersistenceEntity, MutableRevisionedEntity { + + @Override + void setRevision(Number revision); + +} diff --git a/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/QueryHelper.java b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/QueryHelper.java new file mode 100644 index 00000000..5d6e93fc --- /dev/null +++ b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/QueryHelper.java @@ -0,0 +1,428 @@ +package com.devonfw.module.jpa.dataaccess.api; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; + +import net.sf.mmm.util.exception.api.IllegalCaseException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.domain.Sort.Order; +import org.springframework.util.StringUtils; + +import com.devonfw.module.basic.common.api.query.LikePatternSyntax; +import com.devonfw.module.basic.common.api.query.StringSearchConfigTo; +import com.devonfw.module.basic.common.api.query.StringSearchOperator; +import com.querydsl.core.JoinExpression; +import com.querydsl.core.types.EntityPath; +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.ComparablePath; +import com.querydsl.core.types.dsl.PathBuilder; +import com.querydsl.core.types.dsl.SimpleExpression; +import com.querydsl.core.types.dsl.StringExpression; +import com.querydsl.jpa.impl.JPAQuery; + +/** + * Class with utility methods for dealing with queries. Either extend this class or use {@link QueryUtil#get()}. + * + * @since 3.0.0 + */ +public class QueryHelper { + + private static final Logger LOG = LoggerFactory.getLogger(QueryHelper.class); + + /** JPA query property to configure the timeout in milliseconds. */ + protected static final String QUERY_PROPERTY_TIMEOUT = "javax.persistence.query.timeout"; + + /** + * @param query the {@link JPAQuery} to modify. + * @param timeout the timeout in milliseconds. + */ + protected void applyTimeout(JPAQuery query, Number timeout) { + + if (timeout != null) { + query.setHint(QUERY_PROPERTY_TIMEOUT, timeout.intValue()); + } + } + + /** + * @param query the {@link JPAQuery} to modify. + * @param expression the {@link StringExpression} to {@link StringExpression#like(String) create the LIKE-clause} + * from. + * @param pattern the pattern for the LIKE-clause to create. + * @param syntax the {@link LikePatternSyntax} of the given {@code pattern}. + * @param ignoreCase - {@code true} to ignore the case, {@code false} otherwise (to search case-sensitive). + * @param matchSubstring - {@code true} to match also if the given {@code pattern} shall also match substrings on the + * given {@link StringExpression}. + */ + protected void whereLike(JPAQuery query, StringExpression expression, String pattern, LikePatternSyntax syntax, + boolean ignoreCase, boolean matchSubstring) { + + BooleanExpression like = newLikeClause(expression, pattern, syntax, ignoreCase, matchSubstring, false); + if (like != null) { + query.where(like); + } + } + + /** + * @param query the {@link JPAQuery} to modify. + * @param expression the {@link StringExpression} to {@link StringExpression#notLike(String) create the NOT + * LIKE-clause} from. + * @param pattern the pattern for the NOT LIKE-clause to create. + * @param syntax the {@link LikePatternSyntax} of the given {@code pattern}. + * @param ignoreCase - {@code true} to ignore the case, {@code false} otherwise (to search case-sensitive). + * @param matchSubstring - {@code true} to match also if the given {@code pattern} shall also match substrings on the + * given {@link StringExpression}. + */ + protected void whereNotLike(JPAQuery query, StringExpression expression, String pattern, LikePatternSyntax syntax, + boolean ignoreCase, boolean matchSubstring) { + + BooleanExpression like = newLikeClause(expression, pattern, syntax, ignoreCase, matchSubstring, true); + if (like != null) { + query.where(like); + } + } + + /** + * @param expression the {@link StringExpression} to {@link StringExpression#like(String) create the LIKE-clause} + * from. + * @param pattern the pattern for the LIKE-clause to create. + * @param syntax the {@link LikePatternSyntax} of the given {@code pattern}. + * @param ignoreCase - {@code true} to ignore the case, {@code false} otherwise (to search case-sensitive). + * @param matchSubstring - {@code true} to match also if the given {@code pattern} shall also match substrings on the + * given {@link StringExpression}. + * @return the LIKE-clause as {@link BooleanExpression}. + */ + protected BooleanExpression newLikeClause(StringExpression expression, String pattern, LikePatternSyntax syntax, + boolean ignoreCase, boolean matchSubstring) { + + return newLikeClause(expression, pattern, syntax, ignoreCase, matchSubstring, false); + } + + /** + * @param expression the {@link StringExpression} to {@link StringExpression#notLike(String) create the NOT + * LIKE-clause} from. + * @param pattern the pattern for the NOT LIKE-clause to create. + * @param syntax the {@link LikePatternSyntax} of the given {@code pattern}. + * @param ignoreCase - {@code true} to ignore the case, {@code false} otherwise (to search case-sensitive). + * @param matchSubstring - {@code true} to match also if the given {@code pattern} shall also match substrings on the + * given {@link StringExpression}. + * @return the NOT LIKE-clause as {@link BooleanExpression}. + */ + protected BooleanExpression newNotLikeClause(StringExpression expression, String pattern, LikePatternSyntax syntax, + boolean ignoreCase, boolean matchSubstring) { + + return newLikeClause(expression, pattern, syntax, ignoreCase, matchSubstring, true); + } + + /** + * @param expression the {@link StringExpression} to {@link StringExpression#like(String) create the LIKE-clause} + * from. + * @param pattern the pattern for the LIKE-clause to create. + * @param syntax the {@link LikePatternSyntax} of the given {@code pattern}. + * @param ignoreCase - {@code true} to ignore the case, {@code false} otherwise (to search case-sensitive). + * @param matchSubstring - {@code true} to match also if the given {@code pattern} shall also match substrings on the + * given {@link StringExpression}. + * @param negate - {@code true} for {@link StringExpression#notLike(String) NOT LIKE}, {@code false} for + * {@link StringExpression#like(String) LIKE}. + * @return the LIKE-clause as {@link BooleanExpression}. + */ + protected BooleanExpression newLikeClause(StringExpression expression, String pattern, LikePatternSyntax syntax, + boolean ignoreCase, boolean matchSubstring, boolean negate) { + + if (syntax == null) { + syntax = LikePatternSyntax.autoDetect(pattern); + if (syntax == null) { + syntax = LikePatternSyntax.SQL; + } + } + String likePattern = LikePatternSyntax.SQL.convert(pattern, syntax, matchSubstring); + StringExpression exp = expression; + if (ignoreCase) { + likePattern = likePattern.toUpperCase(Locale.US); + exp = exp.upper(); + } + BooleanExpression clause; + if (syntax != LikePatternSyntax.SQL) { + clause = exp.like(likePattern, LikePatternSyntax.ESCAPE); + } else { + clause = exp.like(likePattern); + } + if (negate) { + clause = clause.not(); + } + return clause; + } + + /** + * @param expression the {@link StringExpression} to search on. + * @param value the string value or pattern to search for. + * @param config the {@link StringSearchConfigTo} to configure the search. May be {@code null} for regular equals + * search as default fallback. + * @return the new {@link BooleanExpression} for the specified string comparison clause. + */ + protected BooleanExpression newStringClause(StringExpression expression, String value, StringSearchConfigTo config) { + + StringSearchOperator operator = StringSearchOperator.EQ; + LikePatternSyntax syntax = null; + boolean ignoreCase = false; + boolean matchSubstring = false; + if (config != null) { + operator = config.getOperator(); + syntax = config.getLikeSyntax(); + ignoreCase = config.isIgnoreCase(); + matchSubstring = config.isMatchSubstring(); + } + return newStringClause(expression, value, operator, syntax, ignoreCase, matchSubstring); + } + + /** + * @param expression the {@link StringExpression} to search on. + * @param value the string value or pattern to search for. + * @param syntax the {@link LikePatternSyntax} of the given {@code pattern}. + * @param operator the {@link StringSearchOperator} used to compare the search string {@code value}. + * @param ignoreCase - {@code true} to ignore the case, {@code false} otherwise (to search case-sensitive). + * @param matchSubstring - {@code true} to match also if the given {@code pattern} shall also match substrings on the + * given {@link StringExpression}. + * @return the new {@link BooleanExpression} for the specified string comparison clause. + */ + protected BooleanExpression newStringClause(StringExpression expression, String value, StringSearchOperator operator, + LikePatternSyntax syntax, boolean ignoreCase, boolean matchSubstring) { + + if (operator == null) { + if (syntax == null) { + syntax = LikePatternSyntax.autoDetect(value); + if (syntax == null) { + operator = StringSearchOperator.EQ; + } else { + operator = StringSearchOperator.LIKE; + } + } else { + operator = StringSearchOperator.LIKE; + } + } + if (matchSubstring && ((operator == StringSearchOperator.EQ) || (operator == StringSearchOperator.NE))) { + if (syntax == null) { + syntax = LikePatternSyntax.SQL; + } + if (operator == StringSearchOperator.EQ) { + operator = StringSearchOperator.LIKE; + } else { + operator = StringSearchOperator.NOT_LIKE; + } + } + String v = value; + if (v == null) { + switch (operator) { + case LIKE: + case EQ: + return expression.isNull(); + case NE: + return expression.isNotNull(); + default: + throw new IllegalArgumentException("Operator " + operator + " does not accept null!"); + } + } else if (v.isEmpty()) { + switch (operator) { + case LIKE: + case EQ: + return expression.isEmpty(); + case NOT_LIKE: + case NE: + return expression.isNotEmpty(); + default: + // continue; + } + } + StringExpression exp = expression; + if (ignoreCase) { + v = v.toUpperCase(Locale.US); + exp = exp.upper(); + } + switch (operator) { + case LIKE: + return newLikeClause(exp, v, syntax, false, matchSubstring, false); + case NOT_LIKE: + return newLikeClause(exp, v, syntax, false, matchSubstring, true); + case EQ: + return exp.eq(v); + case NE: + return exp.ne(v); + case LT: + return exp.lt(v); + case LE: + return exp.loe(v); + case GT: + return exp.gt(v); + case GE: + return exp.goe(v); + default: + throw new IllegalCaseException(StringSearchOperator.class, operator); + } + } + + /** + * @param query the {@link JPAQuery} to modify. + * @param expression the {@link StringExpression} to search on. + * @param value the string value or pattern to search for. + * @param config the {@link StringSearchConfigTo} to configure the search. May be {@code null} for regular equals + * search. + */ + protected void whereString(JPAQuery query, StringExpression expression, String value, + StringSearchConfigTo config) { + + BooleanExpression clause = newStringClause(expression, value, config); + if (clause != null) { + query.where(clause); + } + } + + /** + * @param generic type of the {@link Collection} values for the {@link SimpleExpression#in(Collection) + * IN-clause}(s). + * @param expression the {@link SimpleExpression} used to create the {@link SimpleExpression#in(Collection) + * IN-clause}(s). + * @param inValues the {@link Collection} of values for the {@link SimpleExpression#in(Collection) IN-clause}(s). + * @return the {@link BooleanExpression} for the {@link SimpleExpression#in(Collection) IN-clause}(s) oder + * {@code null} if {@code inValues} is {@code null} or {@link Collection#isEmpty() empty}. + */ + protected BooleanExpression newInClause(SimpleExpression expression, Collection inValues) { + + if ((inValues == null) || (inValues.isEmpty())) { + return null; + } + int size = inValues.size(); + if (size == 1) { + return expression.eq(inValues.iterator().next()); + } + int maxSizeOfInClause = DatabaseConfigProperties.getInstance().getMaxSizeOfInClause(); + if (size <= maxSizeOfInClause) { + return expression.in(inValues); + } + LOG.warn("Creating workaround for IN-clause with {} items - this can cause performance problems.", size); + List values; + if (inValues instanceof List) { + values = (List) inValues; + } else { + values = new ArrayList<>(inValues); + } + BooleanExpression predicate = null; + int start = 0; + while (start < size) { + int end = start + maxSizeOfInClause; + if (end > size) { + end = size; + } + List partition = values.subList(start, end); + if (predicate == null) { + predicate = expression.in(partition); + } else { + predicate = predicate.or(expression.in(partition)); + } + start = end; + } + return predicate; + } + + /** + * @param generic type of the {@link Collection} values for the {@link SimpleExpression#in(Collection) + * IN-clause}(s). + * @param query the {@link JPAQuery} where to {@link JPAQuery#where(com.querydsl.core.types.Predicate) add} the + * {@link #newInClause(SimpleExpression, Collection) IN-clause(s)} from the other given parameters. + * @param expression the {@link SimpleExpression} used to create the {@link SimpleExpression#in(Collection) + * IN-clause}(s). + * @param inValues the {@link Collection} of values for the {@link SimpleExpression#in(Collection) IN-clause}(s). + * @see #newInClause(SimpleExpression, Collection) + */ + protected void whereIn(JPAQuery query, SimpleExpression expression, Collection inValues) { + + BooleanExpression inClause = newInClause(expression, inValues); + if (inClause != null) { + query.where(inClause); + } + } + + /** + * Returns a {@link Page} of entities according to the supplied {@link Pageable} and {@link JPAQuery}. + * + * @param generic type of the entity. + * @param pageable contains information about the requested page and sorting. + * @param query is a query which is pre-configured with the desired conditions for the search. + * @param determineTotal - {@code true} to determine the {@link Page#getTotalElements() total number of hits}, + * {@code false} otherwise. + * @return a paginated list. + */ + protected Page findPaginatedGeneric(Pageable pageable, JPAQuery query, boolean determineTotal) { + + long total = -1; + if (determineTotal) { + total = query.clone().fetchCount(); + } + long offset = 0; + if (pageable != null) { + offset = pageable.getOffset(); + query.offset(offset); + query.limit(pageable.getPageSize()); + applySort(query, pageable.getSort()); + } + List hits = query.fetch(); + if (total == -1) { + total = offset + hits.size(); + } + return new PageImpl<>(hits, pageable, total); + } + + /** + * @param query the {@link JPAQuery} to apply the {@link Sort} to. + * @param sort the {@link Sort} to apply as ORDER BY to the given {@link JPAQuery}. + */ + @SuppressWarnings("rawtypes") + protected void applySort(JPAQuery query, Sort sort) { + + if (sort == null) { + return; + } + PathBuilder alias = findAlias(query); + for (Order order : sort) { + String property = order.getProperty(); + Direction direction = order.getDirection(); + ComparablePath path = alias.getComparable(property, Comparable.class); + OrderSpecifier orderSpecifier; + if (direction == Direction.ASC) { + orderSpecifier = path.asc(); + } else { + orderSpecifier = path.desc(); + } + query.orderBy(orderSpecifier); + } + } + + private PathBuilder findAlias(JPAQuery query) { + + String alias = null; + List joins = query.getMetadata().getJoins(); + if ((joins != null) && !joins.isEmpty()) { + JoinExpression join = joins.get(0); + Expression target = join.getTarget(); + if (target instanceof EntityPath) { + alias = target.toString(); // no safe API + } + } + Class type = query.getType(); + if (alias == null) { + // should actually never happen, but fallback is provided as buest guess + alias = StringUtils.uncapitalize(type.getSimpleName()); + } + return new PathBuilder<>(type, alias); + } + +} diff --git a/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/QueryUtil.java b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/QueryUtil.java new file mode 100644 index 00000000..e93724fb --- /dev/null +++ b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/QueryUtil.java @@ -0,0 +1,114 @@ +package com.devonfw.module.jpa.dataaccess.api; + +import java.util.Collection; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import com.devonfw.module.basic.common.api.query.LikePatternSyntax; +import com.devonfw.module.basic.common.api.query.StringSearchConfigTo; +import com.devonfw.module.basic.common.api.query.StringSearchOperator; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.SimpleExpression; +import com.querydsl.core.types.dsl.StringExpression; +import com.querydsl.jpa.impl.JPAQuery; + +/** + * Helper class for {@link #get() static access} to features of {@link QueryHelper}. + * + * @since 3.0.0 + */ +public class QueryUtil extends QueryHelper { + + private static final QueryUtil INSTANCE = new QueryUtil(); + + @Override + public void whereLike(JPAQuery query, StringExpression expression, String pattern, LikePatternSyntax syntax, + boolean ignoreCase, boolean matchSubstring) { + + super.whereLike(query, expression, pattern, syntax, ignoreCase, matchSubstring); + } + + @Override + public BooleanExpression newLikeClause(StringExpression expression, String pattern, LikePatternSyntax syntax, + boolean ignoreCase, boolean matchSubstring, boolean negate) { + + return super.newLikeClause(expression, pattern, syntax, ignoreCase, matchSubstring, negate); + } + + @Override + public BooleanExpression newStringClause(StringExpression expression, String value, StringSearchConfigTo config) { + + return super.newStringClause(expression, value, config); + } + + @Override + public BooleanExpression newStringClause(StringExpression expression, String value, StringSearchOperator operator, + LikePatternSyntax syntax, boolean ignoreCase, boolean matchSubstring) { + + return super.newStringClause(expression, value, operator, syntax, ignoreCase, matchSubstring); + } + + @Override + public void whereString(JPAQuery query, StringExpression expression, String value, StringSearchConfigTo config) { + + super.whereString(query, expression, value, config); + } + + @Override + public void whereNotLike(JPAQuery query, StringExpression expression, String pattern, LikePatternSyntax syntax, + boolean ignoreCase, boolean matchSubstring) { + + super.whereNotLike(query, expression, pattern, syntax, ignoreCase, matchSubstring); + } + + @Override + public BooleanExpression newLikeClause(StringExpression expression, String pattern, LikePatternSyntax syntax, + boolean ignoreCase, boolean matchSubstring) { + + return super.newLikeClause(expression, pattern, syntax, ignoreCase, matchSubstring); + } + + @Override + public BooleanExpression newNotLikeClause(StringExpression expression, String pattern, LikePatternSyntax syntax, + boolean ignoreCase, boolean matchSubstring) { + + return super.newNotLikeClause(expression, pattern, syntax, ignoreCase, matchSubstring); + } + + @Override + public BooleanExpression newInClause(SimpleExpression expression, Collection inValues) { + + return super.newInClause(expression, inValues); + } + + @Override + public void whereIn(JPAQuery query, SimpleExpression expression, Collection inValues) { + + super.whereIn(query, expression, inValues); + } + + /** + * Returns a {@link Page} of entities according to the supplied {@link Pageable} and {@link JPAQuery}. + * + * @param generic type of the entity. + * @param pageable contains information about the requested page and sorting. + * @param query is a query which is pre-configured with the desired conditions for the search. + * @param determineTotal - {@code true} to determine the {@link Page#getTotalElements() total number of hits}, + * {@code false} otherwise. + * @return a paginated list. + */ + public Page findPaginated(Pageable pageable, JPAQuery query, boolean determineTotal) { + + return findPaginatedGeneric(pageable, query, determineTotal); + } + + /** + * @return the {@link QueryUtil} instance. + */ + public static QueryUtil get() { + + return INSTANCE; + } + +} diff --git a/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/RevisionMetadata.java b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/RevisionMetadata.java new file mode 100644 index 00000000..0937b5b3 --- /dev/null +++ b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/RevisionMetadata.java @@ -0,0 +1,36 @@ +/* Copyright (c) The m-m-m Team, Licensed under the Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0 */ +package com.devonfw.module.jpa.dataaccess.api; + +import java.util.Date; + +/** + * This is the interface for the metadata associated with a + * {@link net.sf.mmm.util.entity.api.RevisionedEntity#getRevision() historic revision} of an + * {@link net.sf.mmm.util.entity.api.RevisionedEntity entity}. + * + */ +public interface RevisionMetadata { + + /** + * This method gets the {@link net.sf.mmm.util.entity.api.RevisionedEntity#getRevision() revision number}. + * + * @return the revision number. + */ + Number getRevision(); + + /** + * This method gets the date when this revision was created (closed). + * + * @return the date of completion or {@code null} if the according entity is the latest revision. + */ + Date getDate(); + + /** + * This method gets the identifier (login) of the author who created this revision. + * + * @return the author. May be {@code null} (if committed outside user scope). + */ + String getAuthor(); + +} diff --git a/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/RevisionMetadataType.java b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/RevisionMetadataType.java new file mode 100644 index 00000000..b1545a8c --- /dev/null +++ b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/RevisionMetadataType.java @@ -0,0 +1,67 @@ +package com.devonfw.module.jpa.dataaccess.api; + +import java.util.Date; + +/** + * Implementation of {@link RevisionMetadata} as immutable type. + */ +public class RevisionMetadataType implements RevisionMetadata { + + private final Number revision; + + private final Date date; + + private final String author; + + /** + * The constructor. + * + * @param revision the {@link #getRevision() revision}. + * @param date the {@link #getDate() date}. + * @param author the {@link #getAuthor() author}. + */ + public RevisionMetadataType(Number revision, Date date, String author) { + + this.revision = revision; + this.date = date; + this.author = author; + } + + @Override + public Number getRevision() { + + return this.revision; + } + + @Override + public Date getDate() { + + return this.date; + } + + @Override + public String getAuthor() { + + return this.author; + } + + /** + * @param revEntity die {@link AdvancedRevisionEntity}. + * @return die Instanz von {@link RevisionMetadataType} bzw. {@code null} falls {@code revision} den Wert {@code null} + * hat. + */ + public static RevisionMetadataType of(AdvancedRevisionEntity revEntity) { + + if (revEntity == null) { + return null; + } + return new RevisionMetadataType(revEntity.getId(), revEntity.getDate(), revEntity.getUserLogin()); + } + + @Override + public String toString() { + + return this.revision + "@" + this.date + " by " + this.author; + } + +} diff --git a/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/feature/FeatureForceIncrementModificationCounter.java b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/feature/FeatureForceIncrementModificationCounter.java new file mode 100644 index 00000000..72f8417c --- /dev/null +++ b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/api/feature/FeatureForceIncrementModificationCounter.java @@ -0,0 +1,33 @@ +package com.devonfw.module.jpa.dataaccess.api.feature; + +import net.sf.mmm.util.entity.api.PersistenceEntity; + +/** + * Feature for DAO or repository to {@link #forceIncrementModificationCounter(Object)}. + * + * @param generic type of the managed {@link PersistenceEntity entity}. Typically implementing + * {@link net.sf.mmm.util.entity.api.PersistenceEntity}. + * + * @since 3.0.0 + */ +public interface FeatureForceIncrementModificationCounter { + + /** + * Enforces to increment the {@link PersistenceEntity#getModificationCounter() modificationCounter} e.g. to enforce + * that a parent object gets locked when its children are modified.
+ * As an example we assume that we have the two optimistic locked entities {@code Order} and its contained + * {@code OrderPosition}. By default the users can modify an {@code Order} and each of its {@code OrderPosition}s in + * parallel without getting a locking conflict. This can be desired. However, it can also be a demand that an + * {@code Order} gets approved and the user doing that is responsible for the total price as the sum of the prices of + * each {@code OrderPosition}. Now if another user is adding or changing an {@code OrderPostion} belonging to that + * {@code Order} in parallel the {@code Order} will get approved but the approved total price will differ from what + * the user has actually seen when he clicked on approve. To prevent this the use-case to modify an + * {@code OrderPosition} can use this method to trigger a locking on the associated {@code Order}. The implication is + * also that two users changing an {@code OrderPosition} associated with the same {@code Order} in parallel will get a + * conflict. + * + * @param entity that is getting checked. + */ + void forceIncrementModificationCounter(E entity); + +} diff --git a/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/base/NamedQueryFactoryBean.java b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/base/NamedQueryFactoryBean.java new file mode 100644 index 00000000..3f823109 --- /dev/null +++ b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/base/NamedQueryFactoryBean.java @@ -0,0 +1,64 @@ +package com.devonfw.module.jpa.dataaccess.base; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.hibernate.Query; +import org.springframework.beans.factory.FactoryBean; + +/** + * ProductWriter allows to get named query from resource NamedQueries.xml. + * It is used for example in job to get sql for JdbcCursorItemReader. + * + */ +public class NamedQueryFactoryBean implements FactoryBean { + + private EntityManager entityManager; + + private String queryName; + + /** + * {@inheritDoc} + */ + @Override + public String getObject() throws Exception { + + return this.entityManager.createNamedQuery(this.queryName).unwrap(Query.class).getQueryString(); + } + + /** + * {@inheritDoc} + */ + @Override + public Class getObjectType() { + + return String.class; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isSingleton() { + + return false; + } + + /** + * @param entityManager the entityManager to set + */ + @PersistenceContext + public void setEntityManager(EntityManager entityManager) { + + this.entityManager = entityManager; + } + + /** + * @param queryName the queryName to set + */ + public void setQueryName(String queryName) { + + this.queryName = queryName; + } + +} diff --git a/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/impl/LazyRevisionMetadata.java b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/impl/LazyRevisionMetadata.java new file mode 100644 index 00000000..2b6e5dd2 --- /dev/null +++ b/modules/jpa-basic/src/main/java/com/devonfw/module/jpa/dataaccess/impl/LazyRevisionMetadata.java @@ -0,0 +1,70 @@ +/* Copyright (c) The m-m-m Team, Licensed under the Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0 */ +package com.devonfw.module.jpa.dataaccess.impl; + +import java.util.Date; + +import javax.persistence.EntityManager; + +import com.devonfw.module.jpa.dataaccess.api.AdvancedRevisionEntity; +import com.devonfw.module.jpa.dataaccess.api.RevisionMetadata; + +/** + * This is a lazy implementation of the {@link RevisionMetadata} interface. + * + */ +public class LazyRevisionMetadata implements RevisionMetadata { + + /** The {@link EntityManager} used to read the metadata. */ + private final EntityManager entityManager; + + /** @see #getRevision() */ + private final Long revision; + + /** @see #getRevisionEntity() */ + private AdvancedRevisionEntity revisionEntity; + + /** + * The constructor. + * + * @param entityManager is the {@link EntityManager} used to fetch metadata. + * @param revision is the {@link #getRevision() revision}. + */ + public LazyRevisionMetadata(EntityManager entityManager, Long revision) { + + super(); + this.entityManager = entityManager; + this.revision = revision; + } + + /** + * @return the revisionEntity + */ + public AdvancedRevisionEntity getRevisionEntity() { + + if (this.revisionEntity == null) { + this.revisionEntity = this.entityManager.find(AdvancedRevisionEntity.class, this.revision); + assert (this.revisionEntity != null); + } + return this.revisionEntity; + } + + @Override + public String getAuthor() { + + return getRevisionEntity().getUserLogin(); + } + + @Override + public Date getDate() { + + return getRevisionEntity().getDate(); + } + + @Override + public Number getRevision() { + + return this.revision; + } + +} diff --git a/modules/jpa-dao/pom.xml b/modules/jpa-dao/pom.xml new file mode 100644 index 00000000..3fd363f0 --- /dev/null +++ b/modules/jpa-dao/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-modules + dev-SNAPSHOT + + com.devonfw.java.modules + devon4j-jpa-dao + ${devon4j.version} + jar + ${project.artifactId} + JPA-based DAO infrastructure of the Open Application Standard Platform for Java (devon4j). + + + + com.devonfw.java.modules + devon4j-jpa-basic + + + com.devonfw.java.modules + devon4j-beanmapping + test + + + org.hibernate + hibernate-entitymanager + true + + + org.hibernate + hibernate-envers + true + + + com.devonfw.java.modules + devon4j-test + test + + + org.springframework + spring-orm + test + + + org.springframework + spring-context + test + + + com.h2database + h2 + test + + + + + \ No newline at end of file diff --git a/modules/jpa-dao/src/main/java/com/devonfw/module/jpa/dataaccess/api/Dao.java b/modules/jpa-dao/src/main/java/com/devonfw/module/jpa/dataaccess/api/Dao.java new file mode 100644 index 00000000..6de047fc --- /dev/null +++ b/modules/jpa-dao/src/main/java/com/devonfw/module/jpa/dataaccess/api/Dao.java @@ -0,0 +1,15 @@ +package com.devonfw.module.jpa.dataaccess.api; + +import net.sf.mmm.util.entity.api.PersistenceEntity; + +/** + * This is a simplified variant of {@link GenericDao} for the suggested and common case that you have a {@link Long} as + * {@link PersistenceEntity#getId() primary key}. + * + * @see GenericDao + * + * @param is the generic type of the {@link PersistenceEntity}. + */ +public interface Dao> extends GenericDao { + +} diff --git a/modules/jpa-dao/src/main/java/com/devonfw/module/jpa/dataaccess/api/GenericDao.java b/modules/jpa-dao/src/main/java/com/devonfw/module/jpa/dataaccess/api/GenericDao.java new file mode 100644 index 00000000..99569223 --- /dev/null +++ b/modules/jpa-dao/src/main/java/com/devonfw/module/jpa/dataaccess/api/GenericDao.java @@ -0,0 +1,115 @@ +package com.devonfw.module.jpa.dataaccess.api; + +import java.util.List; + +import net.sf.mmm.util.entity.api.PersistenceEntity; +import net.sf.mmm.util.exception.api.ObjectNotFoundUserException; + +import com.devonfw.module.basic.common.api.reference.Ref; +import com.devonfw.module.jpa.dataaccess.api.feature.FeatureForceIncrementModificationCounter; + +/** + * This is the interface for a Data Access Object (DAO). It acts as a manager responsible for the persistence + * operations on a specific {@link PersistenceEntity entity} {@literal }.
+ * This is base interface contains the CRUD operations: + *

    + *
  • Create: call {@link #save(PersistenceEntity)} on a new entity.
  • + *
  • Retrieve: use find* methods such as {@link #findOne(Object)}. More specific queries will be added in + * dedicated DAO interfaces.
  • + *
  • Update: done automatically by JPA vendor (hibernate) on commit or call {@link #save(PersistenceEntity)} to + * {@link javax.persistence.EntityManager#merge(Object) merge} an entity.
  • + *
  • Delete: call {@link #delete(PersistenceEntity)} or {@link #delete(Object)}.
  • + *
+ * For each (non-abstract) implementation of {@link PersistenceEntity entity} MyEntity you should create an + * interface interface MyEntityDao that inherits from this {@link GenericDao} interface. Also you create an + * implementation of that interface MyEntityDaoImpl that you derive from + * {@link com.devonfw.module.jpa.dataaccess.base.AbstractGenericDao}. + * + * @param is the generic type if the {@link PersistenceEntity#getId() primary key}. + * @param is the generic type of the {@link PersistenceEntity}. + * + */ +public interface GenericDao> extends FeatureForceIncrementModificationCounter { + + /** + * Saves a given entity. Use the returned instance for further operations as the save operation might have changed the + * entity instance completely. + * + * @param entity the {@link PersistenceEntity entity} to save + * @return the saved entity + */ + E save(E entity); + + /** + * Saves all given entities. + * + * @param entities the {@link PersistenceEntity entities} to save + */ + void save(Iterable entities); + + /** + * Retrieves an entity by its id. + * + * @param id must not be {@literal null}. + * @return the entity with the given id or {@literal null} if none found + * @throws ObjectNotFoundUserException if the requested entity does not exists (use {@link #findOne(Object)} to + * prevent). + */ + E find(ID id) throws ObjectNotFoundUserException; + + /** + * Retrieves an entity by its id. + * + * @param id must not be {@literal null}. + * @return the entity with the given id or {@literal null} if none found + * @throws IllegalArgumentException if {@code id} is {@code null} + */ + E findOne(ID id) throws IllegalArgumentException; + + /** + * @param reference the {@link Ref} to the {@link PersistenceEntity} to get. Typically an instance of + * {@link com.devonfw.module.basic.common.api.reference.IdRef}. + * @return the {@link PersistenceEntity} as {@link javax.persistence.EntityManager#getReference(Class, Object) + * reference} for the given {@link Ref}. Will be {@code null} if the given {@link Ref} was {@code null}. + */ + E get(Ref reference); + + /** + * Returns whether an entity with the given id exists. + * + * @param id must not be {@literal null}. + * @return true if an entity with the given id exists, {@literal false} otherwise + */ + boolean exists(ID id); + + /** + * Returns all instances of the type with the given IDs. + * + * @param ids are the IDs of all entities to retrieve e.g. as {@link java.util.List}. + * @return an {@link Iterable} with all {@link PersistenceEntity entites} for the given ids. + */ + List findAll(Iterable ids); + + /** + * Deletes the entity with the given id. + * + * @param id must not be {@literal null}. + * @throws IllegalArgumentException in case the given {@code id} is {@code null} + */ + void delete(ID id) throws IllegalArgumentException; + + /** + * Deletes a given entity. + * + * @param entity the {@link PersistenceEntity entity} to delete + */ + void delete(E entity); + + /** + * Deletes the given entities. + * + * @param entities the {@link PersistenceEntity entities} to delete + */ + void delete(Iterable entities); + +} diff --git a/modules/jpa-dao/src/main/java/com/devonfw/module/jpa/dataaccess/api/MasterDataDao.java b/modules/jpa-dao/src/main/java/com/devonfw/module/jpa/dataaccess/api/MasterDataDao.java new file mode 100644 index 00000000..b8f60848 --- /dev/null +++ b/modules/jpa-dao/src/main/java/com/devonfw/module/jpa/dataaccess/api/MasterDataDao.java @@ -0,0 +1,27 @@ +package com.devonfw.module.jpa.dataaccess.api; + +import java.util.List; + +import net.sf.mmm.util.entity.api.PersistenceEntity; + +/** + * This is the interface for a {@link Dao} responsible for a {@link PersistenceEntity} that represents master-data. In + * that case you typically have a limited number of entities in your persistent store and need operations like + * {@link #findAll()}.
+ * ATTENTION:
+ * Such operations are not part of {@link GenericDao} or {@link Dao} as invoking them (accidently) could cause that an + * extraordinary large number of entities are loaded into main memory and could cause serious performance and stability + * disasters. So only extend this interface in case you are aware of what you are doing. + * + * @param is the generic type of the {@link PersistenceEntity}. + * + */ +public interface MasterDataDao> extends Dao { + + /** + * @return an {@link Iterable} with ALL managed entities from the persistent store. Not exposed to API by default as + * this might not make sense for all kind of entities. + */ + List findAll(); + +} diff --git a/modules/jpa-dao/src/main/java/com/devonfw/module/jpa/dataaccess/api/MutablePersistenceEntity.java b/modules/jpa-dao/src/main/java/com/devonfw/module/jpa/dataaccess/api/MutablePersistenceEntity.java new file mode 100644 index 00000000..5e955169 --- /dev/null +++ b/modules/jpa-dao/src/main/java/com/devonfw/module/jpa/dataaccess/api/MutablePersistenceEntity.java @@ -0,0 +1,18 @@ +/* Copyright (c) The m-m-m Team, Licensed under the Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0 */ +package com.devonfw.module.jpa.dataaccess.api; + +import net.sf.mmm.util.entity.api.MutableRevisionedEntity; +import net.sf.mmm.util.entity.api.PersistenceEntity; + +/** + * This is the interface for a {@link PersistenceEntity} in Devon4j. + * + * @param is the type of the {@link #getId() primary key}. + */ +public interface MutablePersistenceEntity extends PersistenceEntity, MutableRevisionedEntity { + + @Override + void setRevision(Number revision); + +} diff --git a/modules/jpa-dao/src/main/java/com/devonfw/module/jpa/dataaccess/base/AbstractDao.java b/modules/jpa-dao/src/main/java/com/devonfw/module/jpa/dataaccess/base/AbstractDao.java new file mode 100644 index 00000000..4d76f209 --- /dev/null +++ b/modules/jpa-dao/src/main/java/com/devonfw/module/jpa/dataaccess/base/AbstractDao.java @@ -0,0 +1,24 @@ +package com.devonfw.module.jpa.dataaccess.base; + +import net.sf.mmm.util.entity.api.PersistenceEntity; + +import com.devonfw.module.jpa.dataaccess.api.Dao; + +/** + * Abstract base implementation of {@link Dao} interface. + * + * @param is the generic type of the {@link PersistenceEntity}. + * + */ +public abstract class AbstractDao> extends AbstractGenericDao + implements Dao { + + /** + * The constructor. + */ + public AbstractDao() { + + super(); + } + +} \ No newline at end of file diff --git a/modules/jpa-dao/src/main/java/com/devonfw/module/jpa/dataaccess/base/AbstractGenericDao.java b/modules/jpa-dao/src/main/java/com/devonfw/module/jpa/dataaccess/base/AbstractGenericDao.java new file mode 100644 index 00000000..3d441a49 --- /dev/null +++ b/modules/jpa-dao/src/main/java/com/devonfw/module/jpa/dataaccess/base/AbstractGenericDao.java @@ -0,0 +1,231 @@ +package com.devonfw.module.jpa.dataaccess.base; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.persistence.EntityManager; +import javax.persistence.EntityNotFoundException; +import javax.persistence.LockModeType; +import javax.persistence.PersistenceContext; +import javax.persistence.TypedQuery; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; + +import net.sf.mmm.util.entity.api.PersistenceEntity; +import net.sf.mmm.util.exception.api.ObjectNotFoundUserException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.devonfw.module.basic.common.api.reference.Ref; +import com.devonfw.module.jpa.dataaccess.api.GenericDao; +import com.devonfw.module.jpa.dataaccess.api.QueryHelper; + +/** + * This is the abstract base-implementation of the {@link GenericDao} interface. + * + * @param is the generic type if the {@link PersistenceEntity#getId() primary key}. + * @param is the generic type of the managed {@link PersistenceEntity}. + */ +public abstract class AbstractGenericDao> extends QueryHelper + implements GenericDao { + + /** Logger instance. */ + private static final Logger LOG = LoggerFactory.getLogger(AbstractGenericDao.class); + + private EntityManager entityManager; + + /** + * The constructor. + */ + public AbstractGenericDao() { + + super(); + } + + /** + * @return the {@link Class} reflecting the managed entity. + */ + protected abstract Class getEntityClass(); + + /** + * @return the {@link EntityManager} instance. + */ + protected EntityManager getEntityManager() { + + return this.entityManager; + } + + /** + * @param entityManager the {@link EntityManager} to inject. + */ + @PersistenceContext + public void setEntityManager(EntityManager entityManager) { + + this.entityManager = entityManager; + } + + /** + * @return the name of the managed entity. + */ + protected String getEntityName() { + + return getEntityClass().getSimpleName(); + } + + @Override + public E save(E entity) { + + if (isNew(entity)) { + getEntityManager().persist(entity); + LOG.debug("Saved new {} with id {}.", getEntityName(), entity.getId()); + return entity; + } else { + if (getEntityManager().find(entity.getClass(), entity.getId()) != null) { + E update = getEntityManager().merge(entity); + LOG.debug("Updated {} with id {}.", getEntityName(), entity.getId()); + return update; + } else { + throw new EntityNotFoundException("Entity not found"); + } + } + } + + /** + * Determines if the given {@link PersistenceEntity} is {@link PersistenceEntity#STATE_NEW new}. + * + * @param entity is the {@link PersistenceEntity} to check. + * @return {@code true} if {@link PersistenceEntity#STATE_NEW new}, {@code false} otherwise (e.g. + * {@link PersistenceEntity#STATE_DETACHED detached} or {@link PersistenceEntity#STATE_MANAGED managed}. + */ + protected boolean isNew(E entity) { + + return entity.getId() == null; + } + + @Override + public void save(Iterable entities) { + + for (E entity : entities) { + save(entity); + } + } + + @Override + public void forceIncrementModificationCounter(E entity) { + + getEntityManager().lock(entity, LockModeType.OPTIMISTIC_FORCE_INCREMENT); + } + + @Override + public E findOne(ID id) { + + E entity = getEntityManager().find(getEntityClass(), id); + return entity; + } + + @Override + public E find(ID id) throws ObjectNotFoundUserException { + + E entity = findOne(id); + if (entity == null) { + throw new ObjectNotFoundUserException(getEntityClass().getSimpleName(), id); + } + return entity; + } + + @Override + public E get(Ref reference) { + + if (reference == null) { + return null; + } + return getEntityManager().getReference(getEntityClass(), reference.getId()); + } + + @Override + public boolean exists(ID id) { + + // pointless... + return findOne(id) != null; + } + + /** + * @return an {@link Iterable} to find ALL {@link #getEntityClass() managed entities} from the persistent store. Not + * exposed to API by default as this might not make sense for all kind of entities. + */ + public List findAll() { + + CriteriaQuery query = getEntityManager().getCriteriaBuilder().createQuery(getEntityClass()); + Root root = query.from(getEntityClass()); + query.select(root); + TypedQuery typedQuery = getEntityManager().createQuery(query); + List resultList = typedQuery.getResultList(); + LOG.debug("Query for all {} objects returned {} hit(s).", getEntityName(), resultList.size()); + return resultList; + } + + @Override + public List findAll(Iterable ids) { + + CriteriaBuilder builder = getEntityManager().getCriteriaBuilder(); + CriteriaQuery query = builder.createQuery(getEntityClass()); + Root root = query.from(getEntityClass()); + query.select(root); + query.where(root.get("id").in(toCollection(ids))); + TypedQuery typedQuery = getEntityManager().createQuery(query); + List resultList = typedQuery.getResultList(); + LOG.debug("Query for selection of {} objects returned {} hit(s).", getEntityName(), resultList.size()); + return resultList; + } + + /** + * @param ids sequence of id + * @return a collection of these ids to use {@link Predicate#in(Collection)} for instance + */ + protected Collection toCollection(Iterable ids) { + + if (ids instanceof Collection) { + return (Collection) ids; + } + + final Collection idsList = new ArrayList<>(); + for (final ID id : ids) { + idsList.add(id); + } + return idsList; + } + + @Override + public void delete(ID id) { + + E entity = getEntityManager().getReference(getEntityClass(), id); + getEntityManager().remove(entity); + LOG.debug("Deleted {} with ID {}.", getEntityName(), id); + } + + @Override + public void delete(E entity) { + + // entity might be detached and could cause trouble in entityManager on remove + if (getEntityManager().contains(entity)) { + getEntityManager().remove(entity); + LOG.debug("Deleted {} with ID {}.", getEntityName(), entity.getId()); + } else { + delete(entity.getId()); + } + + } + + @Override + public void delete(Iterable entities) { + + for (E entity : entities) { + delete(entity); + } + } + +} \ No newline at end of file diff --git a/modules/jpa-dao/src/main/java/com/devonfw/module/jpa/dataaccess/base/AbstractMasterDataDao.java b/modules/jpa-dao/src/main/java/com/devonfw/module/jpa/dataaccess/base/AbstractMasterDataDao.java new file mode 100644 index 00000000..245f614f --- /dev/null +++ b/modules/jpa-dao/src/main/java/com/devonfw/module/jpa/dataaccess/base/AbstractMasterDataDao.java @@ -0,0 +1,31 @@ +package com.devonfw.module.jpa.dataaccess.base; + +import java.util.List; + +import net.sf.mmm.util.entity.api.PersistenceEntity; + +import com.devonfw.module.jpa.dataaccess.api.MasterDataDao; + +/** + * This is the abstract base implementation of {@link MasterDataDao}. + * + * @param is the generic type of the {@link PersistenceEntity}. + */ +public abstract class AbstractMasterDataDao> extends AbstractDao + implements MasterDataDao { + + /** + * The constructor. + */ + public AbstractMasterDataDao() { + + super(); + } + + @Override + public List findAll() { + + return super.findAll(); + } + +} diff --git a/modules/jpa-dao/src/test/java/com/devonfw/example/TestApplication.java b/modules/jpa-dao/src/test/java/com/devonfw/example/TestApplication.java new file mode 100644 index 00000000..8695af7d --- /dev/null +++ b/modules/jpa-dao/src/test/java/com/devonfw/example/TestApplication.java @@ -0,0 +1,30 @@ +package com.devonfw.example; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.Bean; + +import com.devonfw.module.jpa.dataaccess.api.JpaInitializer; + +/** + * Spring-boot app for testing. + */ +@SpringBootApplication +@EntityScan +public class TestApplication { + + @Bean + public JpaInitializer jpaInitializer() { + + return new JpaInitializer(); + } + + /** + * @param args the command-line arguments + */ + public static void main(String[] args) { + + SpringApplication.run(TestApplication.class, args); + } +} diff --git a/modules/jpa-dao/src/test/java/com/devonfw/example/component/common/api/Bar.java b/modules/jpa-dao/src/test/java/com/devonfw/example/component/common/api/Bar.java new file mode 100644 index 00000000..4f75acdb --- /dev/null +++ b/modules/jpa-dao/src/test/java/com/devonfw/example/component/common/api/Bar.java @@ -0,0 +1,11 @@ +package com.devonfw.example.component.common.api; + +import com.devonfw.example.general.common.api.TestApplicationEntity; + +public interface Bar extends TestApplicationEntity { + + String getMessage(); + + void setMessage(String message); + +} diff --git a/modules/jpa-dao/src/test/java/com/devonfw/example/component/common/api/Foo.java b/modules/jpa-dao/src/test/java/com/devonfw/example/component/common/api/Foo.java new file mode 100644 index 00000000..24b195fa --- /dev/null +++ b/modules/jpa-dao/src/test/java/com/devonfw/example/component/common/api/Foo.java @@ -0,0 +1,16 @@ +package com.devonfw.example.component.common.api; + +import com.devonfw.example.general.common.api.TestApplicationEntity; +import com.devonfw.module.basic.common.api.reference.IdRef; + +public interface Foo extends TestApplicationEntity { + + String getName(); + + void setName(String name); + + IdRef getBarId(); + + void setBarId(IdRef barId); + +} diff --git a/modules/jpa-dao/src/test/java/com/devonfw/example/component/common/api/to/BarEto.java b/modules/jpa-dao/src/test/java/com/devonfw/example/component/common/api/to/BarEto.java new file mode 100644 index 00000000..454a48fc --- /dev/null +++ b/modules/jpa-dao/src/test/java/com/devonfw/example/component/common/api/to/BarEto.java @@ -0,0 +1,27 @@ +package com.devonfw.example.component.common.api.to; + +import com.devonfw.example.component.common.api.Bar; +import com.devonfw.module.basic.common.api.to.AbstractEto; + +/** + * Implementation of {@link Bar} as {@link AbstractEto ETO}. + */ +public class BarEto extends AbstractEto implements Bar { + + private static final long serialVersionUID = 1L; + + private String message; + + @Override + public String getMessage() { + + return this.message; + } + + @Override + public void setMessage(String message) { + + this.message = message; + } + +} diff --git a/modules/jpa-dao/src/test/java/com/devonfw/example/component/common/api/to/FooEto.java b/modules/jpa-dao/src/test/java/com/devonfw/example/component/common/api/to/FooEto.java new file mode 100644 index 00000000..6156c376 --- /dev/null +++ b/modules/jpa-dao/src/test/java/com/devonfw/example/component/common/api/to/FooEto.java @@ -0,0 +1,42 @@ +package com.devonfw.example.component.common.api.to; + +import com.devonfw.example.component.common.api.Bar; +import com.devonfw.example.component.common.api.Foo; +import com.devonfw.module.basic.common.api.reference.IdRef; +import com.devonfw.module.basic.common.api.to.AbstractEto; + +/** + * Implementation of {@link Foo} as {@link AbstractEto ETO}. + */ +public class FooEto extends AbstractEto implements Foo { + private static final long serialVersionUID = 1L; + + private String name; + + private IdRef barId; + + @Override + public String getName() { + + return this.name; + } + + @Override + public void setName(String name) { + + this.name = name; + } + + @Override + public IdRef getBarId() { + + return this.barId; + } + + @Override + public void setBarId(IdRef barId) { + + this.barId = barId; + } + +} diff --git a/modules/jpa-dao/src/test/java/com/devonfw/example/component/dataaccess/api/BarEntity.java b/modules/jpa-dao/src/test/java/com/devonfw/example/component/dataaccess/api/BarEntity.java new file mode 100644 index 00000000..4b698377 --- /dev/null +++ b/modules/jpa-dao/src/test/java/com/devonfw/example/component/dataaccess/api/BarEntity.java @@ -0,0 +1,31 @@ +package com.devonfw.example.component.dataaccess.api; + +import javax.persistence.Entity; +import javax.persistence.Table; + +import com.devonfw.example.component.common.api.Bar; +import com.devonfw.example.general.dataaccess.api.TestApplicationPersistenceEntity; + +/** + * Implementation of {@link Bar} as {@link TestApplicationPersistenceEntity persistence entity}. + */ +@Entity +@Table(name = "Bar") +public class BarEntity extends TestApplicationPersistenceEntity implements Bar { + private static final long serialVersionUID = 1L; + + private String message; + + @Override + public String getMessage() { + + return this.message; + } + + @Override + public void setMessage(String message) { + + this.message = message; + } + +} diff --git a/modules/jpa-dao/src/test/java/com/devonfw/example/component/dataaccess/api/FooEntity.java b/modules/jpa-dao/src/test/java/com/devonfw/example/component/dataaccess/api/FooEntity.java new file mode 100644 index 00000000..e7f18a57 --- /dev/null +++ b/modules/jpa-dao/src/test/java/com/devonfw/example/component/dataaccess/api/FooEntity.java @@ -0,0 +1,76 @@ +package com.devonfw.example.component.dataaccess.api; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.OneToOne; +import javax.persistence.Table; +import javax.persistence.Transient; + +import com.devonfw.example.component.common.api.Bar; +import com.devonfw.example.component.common.api.Foo; +import com.devonfw.example.general.dataaccess.api.TestApplicationPersistenceEntity; +import com.devonfw.module.basic.common.api.reference.IdRef; +import com.devonfw.module.jpa.dataaccess.api.JpaHelper; + +/** + * Implementation of {@link Foo} as {@link TestApplicationPersistenceEntity persistence entity}. + */ +@Entity +@Table(name = "Foo") +public class FooEntity extends TestApplicationPersistenceEntity implements Foo { + private static final long serialVersionUID = 1L; + + private String name; + + private BarEntity bar; + + @Override + public String getName() { + + return this.name; + } + + @Override + public void setName(String name) { + + this.name = name; + } + + /** + * @return the {@link BarEntity}. + */ + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @JoinColumn(name = "bar") + public BarEntity getBar() { + + return this.bar; + } + + /** + * @param bar new value of {@link #getBar()}. + */ + public void setBar(BarEntity bar) { + + this.bar = bar; + } + + @Override + @Transient + public IdRef getBarId() { + + // you actually need Java8 type-inference to use this feature in a comfortable way as otherwise you need to qualify + // the generic on every use + + // return IdRef.of(this.bar); // Java 8+ + return IdRef. of(this.bar); // Java 5/6/7 + } + + @Override + public void setBarId(IdRef barId) { + + this.bar = JpaHelper.asEntity(barId, BarEntity.class); + } + +} diff --git a/modules/jpa-dao/src/test/java/com/devonfw/example/component/dataaccess/api/dao/BarDao.java b/modules/jpa-dao/src/test/java/com/devonfw/example/component/dataaccess/api/dao/BarDao.java new file mode 100644 index 00000000..1f21d151 --- /dev/null +++ b/modules/jpa-dao/src/test/java/com/devonfw/example/component/dataaccess/api/dao/BarDao.java @@ -0,0 +1,11 @@ +package com.devonfw.example.component.dataaccess.api.dao; + +import com.devonfw.example.component.dataaccess.api.BarEntity; +import com.devonfw.module.jpa.dataaccess.api.Dao; + +/** + * {@link Dao} for {@link BarEntity}. + */ +public interface BarDao extends Dao { + +} diff --git a/modules/jpa-dao/src/test/java/com/devonfw/example/component/dataaccess/impl/BarDaoTxBean.java b/modules/jpa-dao/src/test/java/com/devonfw/example/component/dataaccess/impl/BarDaoTxBean.java new file mode 100644 index 00000000..5bd4a748 --- /dev/null +++ b/modules/jpa-dao/src/test/java/com/devonfw/example/component/dataaccess/impl/BarDaoTxBean.java @@ -0,0 +1,52 @@ +package com.devonfw.example.component.dataaccess.impl; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.transaction.Transactional; + +import com.devonfw.example.component.dataaccess.api.BarEntity; +import com.devonfw.example.component.dataaccess.api.dao.BarDao; +import com.devonfw.module.jpa.dataaccess.api.GenericDao; +import com.devonfw.module.jpa.dataaccess.base.AbstractGenericDaoTest; + +/** + * This type provides methods in a transactional environment for the {@link AbstractGenericDaoTest}. All methods, + * annotated with the {@link Transactional} annotation, are executed in separate transaction, thus one test case can + * execute multiple transactions. Unfortunately this does not work when the transactional methods are directly in the + * top-level class of the test-case itself. + */ +@Named +public class BarDaoTxBean { + + @Inject + private BarDao genericDao; + + /** + * Creates a new {@link BarEntity}, persist it and surround everything with a transaction. + * + * @return entity the new {@link BarEntity}. + */ + @Transactional + public BarEntity create() { + + BarEntity entity = new BarEntity(); + this.genericDao.save(entity); + return entity; + } + + /** + * Loads the {@link BarEntity} with the given {@code id} and + * {@link GenericDao#forceIncrementModificationCounter(net.sf.mmm.util.entity.api.PersistenceEntity) increments the + * modification counter}. + * + * @param id of the {@link BarEntity} to load and increment. + * @return entity the updated {@link BarEntity}. + */ + @Transactional + public BarEntity incrementModificationCounter(long id) { + + BarEntity entity = this.genericDao.find(id); + this.genericDao.forceIncrementModificationCounter(entity); + return entity; + } +} \ No newline at end of file diff --git a/modules/jpa-dao/src/test/java/com/devonfw/example/component/dataaccess/impl/dao/BarDaoImpl.java b/modules/jpa-dao/src/test/java/com/devonfw/example/component/dataaccess/impl/dao/BarDaoImpl.java new file mode 100644 index 00000000..71db90f3 --- /dev/null +++ b/modules/jpa-dao/src/test/java/com/devonfw/example/component/dataaccess/impl/dao/BarDaoImpl.java @@ -0,0 +1,21 @@ +package com.devonfw.example.component.dataaccess.impl.dao; + +import javax.inject.Named; + +import com.devonfw.example.component.dataaccess.api.BarEntity; +import com.devonfw.example.component.dataaccess.api.dao.BarDao; +import com.devonfw.module.jpa.dataaccess.base.AbstractDao; + +/** + * Implementation of {@link BarDao}. + */ +@Named +public class BarDaoImpl extends AbstractDao implements BarDao { + + @Override + protected Class getEntityClass() { + + return BarEntity.class; + } + +} diff --git a/modules/jpa-dao/src/test/java/com/devonfw/example/general/common/api/TestApplicationEntity.java b/modules/jpa-dao/src/test/java/com/devonfw/example/general/common/api/TestApplicationEntity.java new file mode 100644 index 00000000..98ae76a9 --- /dev/null +++ b/modules/jpa-dao/src/test/java/com/devonfw/example/general/common/api/TestApplicationEntity.java @@ -0,0 +1,11 @@ +package com.devonfw.example.general.common.api; + +import net.sf.mmm.util.entity.api.MutableGenericEntity; + +/** + * This is the abstract interface for a {@link MutableGenericEntity} of this application. We are using {@link Long} for + * all {@link #getId() primary keys}. + */ +public abstract interface TestApplicationEntity extends MutableGenericEntity { + +} diff --git a/modules/jpa-dao/src/test/java/com/devonfw/example/general/common/api/config/BeansDozerConfig.java b/modules/jpa-dao/src/test/java/com/devonfw/example/general/common/api/config/BeansDozerConfig.java new file mode 100644 index 00000000..f578749d --- /dev/null +++ b/modules/jpa-dao/src/test/java/com/devonfw/example/general/common/api/config/BeansDozerConfig.java @@ -0,0 +1,32 @@ +package com.devonfw.example.general.common.api.config; + +import java.util.ArrayList; +import java.util.List; + +import org.dozer.DozerBeanMapper; +import org.dozer.Mapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +/** + * Java bean configuration for Dozer + */ +@Configuration +@ComponentScan(basePackages = { "com.devonfw.module.beanmapping" }) +public class BeansDozerConfig { + + private static final String DOZER_MAPPING_XML = "config/app/common/dozer-mapping.xml"; + + /** + * @return the {@link DozerBeanMapper}. + */ + @Bean + public Mapper getDozer() { + + List beanMappings = new ArrayList<>(); + beanMappings.add(DOZER_MAPPING_XML); + return new DozerBeanMapper(beanMappings); + + } +} diff --git a/modules/jpa-dao/src/test/java/com/devonfw/example/general/dataaccess/api/TestApplicationPersistenceEntity.java b/modules/jpa-dao/src/test/java/com/devonfw/example/general/dataaccess/api/TestApplicationPersistenceEntity.java new file mode 100644 index 00000000..2426da63 --- /dev/null +++ b/modules/jpa-dao/src/test/java/com/devonfw/example/general/dataaccess/api/TestApplicationPersistenceEntity.java @@ -0,0 +1,108 @@ +package com.devonfw.example.general.dataaccess.api; + +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.MappedSuperclass; +import javax.persistence.Transient; +import javax.persistence.Version; + +import com.devonfw.example.general.common.api.TestApplicationEntity; +import com.devonfw.module.jpa.dataaccess.api.MutablePersistenceEntity; + +/** + * Abstract Entity for all Entities with an id and a version field. + * + */ +@MappedSuperclass +public abstract class TestApplicationPersistenceEntity implements TestApplicationEntity, MutablePersistenceEntity { + + private static final long serialVersionUID = 1L; + + /** @see #getId() */ + private Long id; + + /** @see #getModificationCounter() */ + private int modificationCounter; + + /** @see #getRevision() */ + private Number revision; + + /** + * The constructor. + */ + public TestApplicationPersistenceEntity() { + + super(); + } + + @Override + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + public Long getId() { + + return this.id; + } + + /** + * {@inheritDoc} + */ + @Override + public void setId(Long id) { + + this.id = id; + } + + @Override + @Version + public int getModificationCounter() { + + return this.modificationCounter; + } + + @Override + public void setModificationCounter(int version) { + + this.modificationCounter = version; + } + + @Override + @Transient + public Number getRevision() { + + return this.revision; + } + + /** + * @param revision the revision to set + */ + @Override + public void setRevision(Number revision) { + + this.revision = revision; + } + + @Override + public String toString() { + + StringBuilder buffer = new StringBuilder(); + toString(buffer); + return buffer.toString(); + } + + /** + * Method to extend {@link #toString()} logic. + * + * @param buffer is the {@link StringBuilder} where to {@link StringBuilder#append(Object) append} the string + * representation. + */ + protected void toString(StringBuilder buffer) { + + buffer.append(getClass().getSimpleName()); + if (this.id != null) { + buffer.append("[id="); + buffer.append(this.id); + buffer.append("]"); + } + } +} \ No newline at end of file diff --git a/modules/jpa-dao/src/test/java/com/devonfw/module/jpa/dataaccess/api/JpaHelperTest.java b/modules/jpa-dao/src/test/java/com/devonfw/module/jpa/dataaccess/api/JpaHelperTest.java new file mode 100644 index 00000000..39efcdfc --- /dev/null +++ b/modules/jpa-dao/src/test/java/com/devonfw/module/jpa/dataaccess/api/JpaHelperTest.java @@ -0,0 +1,61 @@ +package com.devonfw.module.jpa.dataaccess.api; + +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.transaction.Transactional; + +import org.junit.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; + +import com.devonfw.example.TestApplication; +import com.devonfw.example.component.common.api.to.FooEto; +import com.devonfw.example.component.dataaccess.api.BarEntity; +import com.devonfw.example.component.dataaccess.api.FooEntity; +import com.devonfw.module.beanmapping.common.api.BeanMapper; +import com.devonfw.module.jpa.dataaccess.api.JpaHelper; +import com.devonfw.module.test.common.base.ComponentTest; + +/** + * Test of {@link JpaHelper}. + */ +@Transactional +@SpringBootTest(classes = { TestApplication.class }, webEnvironment = WebEnvironment.NONE) +public class JpaHelperTest extends ComponentTest { + + @PersistenceContext + private EntityManager entityManager; + + @Inject + private BeanMapper beanMapper; + + /** + * Test of {@link JpaHelper#asEntity(com.devonfw.module.basic.common.api.reference.Ref, Class)} via real production-like + * scenario. + */ + @Test + public void testIdRefAsEntity() { + + // given + BarEntity barEntity = new BarEntity(); + barEntity.setMessage("Test message"); + FooEntity fooEntity = new FooEntity(); + fooEntity.setName("Test name"); + fooEntity.setBar(barEntity); + + // when + this.entityManager.persist(fooEntity); + FooEto fooEto = new FooEto(); + fooEto.setId(fooEntity.getId()); + fooEto.setModificationCounter(fooEntity.getModificationCounter()); + fooEto.setName(fooEntity.getName()); + fooEto.setBarId(fooEntity.getBarId()); + + FooEntity fooEntity2 = this.beanMapper.map(fooEto, FooEntity.class); + + // then + assertThat(fooEntity2.getBar()).isSameAs(barEntity); + } + +} diff --git a/modules/jpa-dao/src/test/java/com/devonfw/module/jpa/dataaccess/base/AbstractGenericDaoTest.java b/modules/jpa-dao/src/test/java/com/devonfw/module/jpa/dataaccess/base/AbstractGenericDaoTest.java new file mode 100644 index 00000000..53640e0c --- /dev/null +++ b/modules/jpa-dao/src/test/java/com/devonfw/module/jpa/dataaccess/base/AbstractGenericDaoTest.java @@ -0,0 +1,43 @@ +package com.devonfw.module.jpa.dataaccess.base; + +import javax.inject.Inject; + +import org.junit.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; + +import com.devonfw.example.TestApplication; +import com.devonfw.example.component.dataaccess.api.BarEntity; +import com.devonfw.example.component.dataaccess.impl.BarDaoTxBean; +import com.devonfw.module.jpa.dataaccess.api.GenericDao; +import com.devonfw.module.test.common.base.ComponentTest; + +/** + * Test class to test the {@link GenericDao}. + */ +@SpringBootTest(classes = { TestApplication.class }, webEnvironment = WebEnvironment.NONE) +public class AbstractGenericDaoTest extends ComponentTest { + + @Inject + private BarDaoTxBean testBean; + + /** + * Test of {@link GenericDao#forceIncrementModificationCounter(Object)}. Ensures that the modification counter is + * updated after the call of that method when the transaction is closed. + */ + @Test + public void testForceIncrementModificationCounter() { + + // given + BarEntity entity = this.testBean.create(); + assertThat(entity.getId()).isNotNull(); + assertThat(entity.getModificationCounter()).isEqualTo(0); + + // when + BarEntity updatedEntity = this.testBean.incrementModificationCounter(entity.getId()); + + // then + assertThat(updatedEntity.getModificationCounter()).isEqualTo(1); + } + +}; diff --git a/modules/jpa-dao/src/test/java/com/devonfw/module/jpa/dataaccess/base/AbstractPersistenceEntity.java b/modules/jpa-dao/src/test/java/com/devonfw/module/jpa/dataaccess/base/AbstractPersistenceEntity.java new file mode 100644 index 00000000..a7af0715 --- /dev/null +++ b/modules/jpa-dao/src/test/java/com/devonfw/module/jpa/dataaccess/base/AbstractPersistenceEntity.java @@ -0,0 +1,106 @@ +package com.devonfw.module.jpa.dataaccess.base; + +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.MappedSuperclass; +import javax.persistence.Transient; +import javax.persistence.Version; + +import com.devonfw.module.jpa.dataaccess.api.MutablePersistenceEntity; + +/** + * Abstract base implementation of {@link MutablePersistenceEntity}. + */ +@MappedSuperclass +public abstract class AbstractPersistenceEntity implements MutablePersistenceEntity { + + private static final long serialVersionUID = 1L; + + /** @see #getId() */ + private Long id; + + /** @see #getModificationCounter() */ + private int modificationCounter; + + /** @see #getRevision() */ + private Number revision; + + /** + * The constructor. + */ + public AbstractPersistenceEntity() { + + super(); + } + + @Override + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + public Long getId() { + + return this.id; + } + + /** + * {@inheritDoc} + */ + @Override + public void setId(Long id) { + + this.id = id; + } + + @Override + @Version + public int getModificationCounter() { + + return this.modificationCounter; + } + + @Override + public void setModificationCounter(int version) { + + this.modificationCounter = version; + } + + @Override + @Transient + public Number getRevision() { + + return this.revision; + } + + /** + * @param revision the revision to set + */ + @Override + public void setRevision(Number revision) { + + this.revision = revision; + } + + @Override + public String toString() { + + StringBuilder buffer = new StringBuilder(); + toString(buffer); + return buffer.toString(); + } + + /** + * Method to extend {@link #toString()} logic. + * + * @param buffer is the {@link StringBuilder} where to {@link StringBuilder#append(Object) append} the string + * representation. + */ + protected void toString(StringBuilder buffer) { + + buffer.append(getClass().getSimpleName()); + if (this.id != null) { + buffer.append("[id="); + buffer.append(this.id); + buffer.append("]"); + } + } +} diff --git a/modules/jpa-dao/src/test/resources/config/app/common/dozer-mapping.xml b/modules/jpa-dao/src/test/resources/config/app/common/dozer-mapping.xml new file mode 100644 index 00000000..6d44f496 --- /dev/null +++ b/modules/jpa-dao/src/test/resources/config/app/common/dozer-mapping.xml @@ -0,0 +1,31 @@ + + + + + + true + + + java.lang.Long + java.lang.Integer + java.lang.Number + com.devonfw.module.basic.common.api.reference.IdRef + + + + + + com.devonfw.example.general.dataaccess.api.TestApplicationPersistenceEntity + com.devonfw.module.basic.common.api.to.AbstractEto + + this + persistentEntity + + + diff --git a/modules/jpa-envers/pom.xml b/modules/jpa-envers/pom.xml new file mode 100644 index 00000000..fac953f7 --- /dev/null +++ b/modules/jpa-envers/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-modules + dev-SNAPSHOT + + com.devonfw.java.modules + devon4j-jpa-envers + ${devon4j.version} + jar + ${project.artifactId} + JPA and envers support for Open Application Standard Platform for Java (devon4j). + + + + ${project.groupId} + devon4j-jpa-dao + + + org.hibernate + hibernate-envers + + + + ${project.groupId} + devon4j-test + test + + + + diff --git a/modules/jpa-envers/src/main/java/com/devonfw/module/jpa/dataaccess/api/GenericRevisionedDao.java b/modules/jpa-envers/src/main/java/com/devonfw/module/jpa/dataaccess/api/GenericRevisionedDao.java new file mode 100644 index 00000000..9181e169 --- /dev/null +++ b/modules/jpa-envers/src/main/java/com/devonfw/module/jpa/dataaccess/api/GenericRevisionedDao.java @@ -0,0 +1,85 @@ +/* Copyright (c) The m-m-m Team, Licensed under the Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0 */ +package com.devonfw.module.jpa.dataaccess.api; + +import java.util.List; + +import javax.persistence.PersistenceException; + +import net.sf.mmm.util.entity.api.RevisionedEntity; +import net.sf.mmm.util.exception.api.ObjectNotFoundException; + +import com.devonfw.module.jpa.dataaccess.api.GenericDao; +import com.devonfw.module.jpa.dataaccess.api.MutablePersistenceEntity; +import com.devonfw.module.jpa.dataaccess.api.RevisionMetadata; + +/** + * This is the interface for a {@link GenericDao} with the ability of revision-control. It organizes a revision-history + * (journal) of the managed entities. + * + * @see RevisionedEntity + * + * @param is the type of the {@link RevisionedEntity#getId() primary key}. + * @param is the type of the managed entity. + * + */ +public interface GenericRevisionedDao> extends GenericDao { + + /** + * This method will get the {@link List} of historic {@link MutablePersistenceEntity#getRevision() revisions} of the + * {@link MutablePersistenceEntity entity} with the given id.
+ * If the entity is NOT revision controlled, an {@link java.util.Collections#emptyList() empty list} is + * returned. + * + * @param id the {@link MutablePersistenceEntity#getId() primary key} of the {@link MutablePersistenceEntity entity} + * to retrieve the history for. + * @return the {@link List} of historic {@link RevisionedEntity#getRevision() revisions}. + */ + List getRevisionHistory(ID id); + + /** + * This method will get the {@link List} of {@link RevisionMetadata} from the {@link RevisionedEntity#getRevision() + * revision}-history of the entity with the given id. + * + * @param id is the {@link RevisionedEntity#getId() primary key} of the entity for which the history-metadata is + * requested. + * @return the requested {@link List} of {@link RevisionMetadata}. + */ + List getRevisionHistoryMetadata(Object id); + + /** + * This method loads a historic {@link RevisionedEntity#getRevision() revision} of the {@link RevisionedEntity} with + * the given id from the persistent store.
+ * However if the given revision is {@link RevisionedEntity#LATEST_REVISION} the {@link #find(Object) + * latest revision will be loaded}.
+ * ATTENTION:
+ * You should not make assumptions about the revision numbering of the underlying implementation. Please + * use {@link #getRevisionHistory(Object)} or {@link #getRevisionHistoryMetadata(Object)} to find revision numbers. + * + * @param id is the {@link RevisionedEntity#getId() primary key} of the requested {@link RevisionedEntity entity}. + * @param revision is the {@link RevisionedEntity#getRevision() revision} of the requested entity or + * {@link RevisionedEntity#LATEST_REVISION} to get the {@link #find(Object) latest} revision. A specific + * revision has to be greater than 0. + * @return the requested {@link RevisionedEntity entity}. + * @throws ObjectNotFoundException if the requested {@link RevisionedEntity entity} could NOT be found. + */ + ENTITY load(ID id, Number revision) throws ObjectNotFoundException; + + /** + * {@inheritDoc} + * + * The behavior of this method depends on the revision-control strategy of the implementation.
+ *
    + *
  • In case of an audit-proof revision-history the deletion of the + * {@link RevisionedEntity#LATEST_REVISION latest revision} of an entity will only move it to the history while the + * deletion of a {@link RevisionedEntity#getRevision() historic entity} is NOT permitted and will cause a + * {@link PersistenceException}.
  • + *
  • In case of an on-demand revision-history the deletion of the {@link RevisionedEntity#LATEST_REVISION + * latest revision} of an entity will either move it to the history or
  • + *
+ * If the given entity is a {@link RevisionedEntity#getRevision() historic entity} the according historic + */ + @Override + void delete(ENTITY entity); + +} diff --git a/modules/jpa-envers/src/main/java/com/devonfw/module/jpa/dataaccess/api/RevisionedDao.java b/modules/jpa-envers/src/main/java/com/devonfw/module/jpa/dataaccess/api/RevisionedDao.java new file mode 100644 index 00000000..06e62a67 --- /dev/null +++ b/modules/jpa-envers/src/main/java/com/devonfw/module/jpa/dataaccess/api/RevisionedDao.java @@ -0,0 +1,14 @@ +package com.devonfw.module.jpa.dataaccess.api; + +import com.devonfw.module.jpa.dataaccess.api.MutablePersistenceEntity; + +/** + * This is a simplified variant of {@link GenericRevisionedDao} for the suggested and common case that you have a + * {@link Long} as {@link MutablePersistenceEntity#getId() primary key}. + * + * @param is the type of the managed {@link MutablePersistenceEntity entity}. + */ +public interface RevisionedDao> extends + GenericRevisionedDao { + +} diff --git a/modules/jpa-envers/src/main/java/com/devonfw/module/jpa/dataaccess/api/RevisionedMasterDataDao.java b/modules/jpa-envers/src/main/java/com/devonfw/module/jpa/dataaccess/api/RevisionedMasterDataDao.java new file mode 100644 index 00000000..812fe075 --- /dev/null +++ b/modules/jpa-envers/src/main/java/com/devonfw/module/jpa/dataaccess/api/RevisionedMasterDataDao.java @@ -0,0 +1,31 @@ +package com.devonfw.module.jpa.dataaccess.api; + +import java.util.List; + +import net.sf.mmm.util.entity.api.PersistenceEntity; + +import com.devonfw.module.jpa.dataaccess.api.Dao; +import com.devonfw.module.jpa.dataaccess.api.GenericDao; +import com.devonfw.module.jpa.dataaccess.api.MutablePersistenceEntity; + +/** + * This is the interface for a {@link Dao} responsible for a {@link PersistenceEntity} that represents master-data. In + * that case you typically have a limited number of entities in your persistent store and need operations like + * {@link #findAll()}.
+ * ATTENTION:
+ * Such operations are not part of {@link GenericDao} or {@link Dao} as invoking them (accidently) could cause that an + * extraordinary large number of entities are loaded into main memory and could cause serious performance and stability + * disasters. So only extend this interface in case you are aware of what you are doing. + * + * @param is the generic type of the {@link PersistenceEntity}. + * + */ +public interface RevisionedMasterDataDao> extends GenericRevisionedDao { + + /** + * @return an {@link Iterable} with ALL managed entities from the persistent store. Not exposed to API by default as + * this might not make sense for all kind of entities. + */ + List findAll(); + +} diff --git a/modules/jpa-envers/src/main/java/com/devonfw/module/jpa/dataaccess/base/AbstractGenericRevisionedDao.java b/modules/jpa-envers/src/main/java/com/devonfw/module/jpa/dataaccess/base/AbstractGenericRevisionedDao.java new file mode 100644 index 00000000..bbcb1009 --- /dev/null +++ b/modules/jpa-envers/src/main/java/com/devonfw/module/jpa/dataaccess/base/AbstractGenericRevisionedDao.java @@ -0,0 +1,93 @@ +/* Copyright (c) The m-m-m Team, Licensed under the Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0 */ +package com.devonfw.module.jpa.dataaccess.base; + +import java.util.ArrayList; +import java.util.List; + +import net.sf.mmm.util.exception.api.ObjectNotFoundException; + +import org.hibernate.envers.AuditReader; +import org.hibernate.envers.AuditReaderFactory; + +import com.devonfw.module.jpa.dataaccess.api.GenericRevisionedDao; +import com.devonfw.module.jpa.dataaccess.api.MutablePersistenceEntity; +import com.devonfw.module.jpa.dataaccess.api.RevisionMetadata; +import com.devonfw.module.jpa.dataaccess.base.AbstractGenericDao; +import com.devonfw.module.jpa.dataaccess.impl.LazyRevisionMetadata; + +/** + * This is the abstract base-implementation of a {@link AbstractGenericDao} using to manage the revision-control. + * + * @param is the type of the {@link MutablePersistenceEntity#getId() primary key} of the managed + * {@link MutablePersistenceEntity entity}. + * @param is the {@link #getEntityClass() type} of the managed entity. + * + */ +public abstract class AbstractGenericRevisionedDao> + extends AbstractGenericDao implements GenericRevisionedDao { + + /** + * The constructor. + */ + public AbstractGenericRevisionedDao() { + + super(); + } + + /** + * @return the auditReader + */ + protected AuditReader getAuditReader() { + + return AuditReaderFactory.get(getEntityManager()); + } + + @Override + public ENTITY load(ID id, Number revision) throws ObjectNotFoundException { + + if (revision == MutablePersistenceEntity.LATEST_REVISION) { + return find(id); + } else { + return loadRevision(id, revision); + } + } + + /** + * This method gets a historic revision of the {@link net.sf.mmm.util.entity.api.GenericEntity} with the given + * id. + * + * @param id is the {@link net.sf.mmm.util.entity.api.GenericEntity#getId() ID} of the requested + * {@link net.sf.mmm.util.entity.api.GenericEntity entity}. + * @param revision is the {@link MutablePersistenceEntity#getRevision() revision} + * @return the requested {@link net.sf.mmm.util.entity.api.GenericEntity entity}. + */ + protected ENTITY loadRevision(Object id, Number revision) { + + Class entityClassImplementation = getEntityClass(); + ENTITY entity = getAuditReader().find(entityClassImplementation, id, revision); + if (entity != null) { + entity.setRevision(revision); + } + return entity; + } + + @Override + public List getRevisionHistory(ID id) { + + return getAuditReader().getRevisions(getEntityClass(), id); + } + + @Override + public List getRevisionHistoryMetadata(Object id) { + + AuditReader auditReader = getAuditReader(); + List revisionList = auditReader.getRevisions(getEntityClass(), id); + List result = new ArrayList<>(); + for (Number revision : revisionList) { + Long revisionLong = Long.valueOf(revision.longValue()); + result.add(new LazyRevisionMetadata(getEntityManager(), revisionLong)); + } + return result; + } +} diff --git a/modules/jpa-envers/src/main/java/com/devonfw/module/jpa/dataaccess/base/AbstractRevisionedDao.java b/modules/jpa-envers/src/main/java/com/devonfw/module/jpa/dataaccess/base/AbstractRevisionedDao.java new file mode 100644 index 00000000..d19e317f --- /dev/null +++ b/modules/jpa-envers/src/main/java/com/devonfw/module/jpa/dataaccess/base/AbstractRevisionedDao.java @@ -0,0 +1,14 @@ +package com.devonfw.module.jpa.dataaccess.base; + +import com.devonfw.module.jpa.dataaccess.api.MutablePersistenceEntity; +import com.devonfw.module.jpa.dataaccess.api.RevisionedDao; + +/** + * Abstract base implementation of {@link RevisionedDao} interface. + * + * @param is the type of the managed {@link MutablePersistenceEntity entity}. + */ +public abstract class AbstractRevisionedDao> extends + AbstractGenericRevisionedDao implements RevisionedDao { + +} diff --git a/modules/jpa-envers/src/main/java/com/devonfw/module/jpa/dataaccess/base/AdvancedRevisionEntity.java b/modules/jpa-envers/src/main/java/com/devonfw/module/jpa/dataaccess/base/AdvancedRevisionEntity.java new file mode 100644 index 00000000..c16eec3b --- /dev/null +++ b/modules/jpa-envers/src/main/java/com/devonfw/module/jpa/dataaccess/base/AdvancedRevisionEntity.java @@ -0,0 +1,129 @@ +/* Copyright (c) The m-m-m Team, Licensed under the Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0 */ +package com.devonfw.module.jpa.dataaccess.base; + +import java.util.Date; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.Table; + +import net.sf.mmm.util.entity.api.PersistenceEntity; + +import org.hibernate.envers.RevisionEntity; +import org.hibernate.envers.RevisionNumber; +import org.hibernate.envers.RevisionTimestamp; + +/** + * This is a custom {@link org.hibernate.envers.DefaultRevisionEntity revision entity} also containing the actual user. + * + * @see org.hibernate.envers.DefaultRevisionEntity + * @deprecated If you want to have the backward compatibility with your existing code , please use this class else if + * you are starting the development of application from scratch, please use + * {@link com.devonfw.module.jpa.dataaccess.api.AdvancedRevisionEntity} + */ +@Entity +@RevisionEntity(AdvancedRevisionListener.class) +@Table(name = "RevInfo") +@Deprecated +public class AdvancedRevisionEntity implements PersistenceEntity { + + /** UID for serialization. */ + private static final long serialVersionUID = 1L; + + /** @see #getId() */ + @Id + @GeneratedValue + @RevisionNumber + private Long id; + + /** @see #getTimestamp() */ + @RevisionTimestamp + private long timestamp; + + /** @see #getDate() */ + private transient Date date; + + /** @see #getUser() */ + + private String user; + + /** + * The constructor. + */ + public AdvancedRevisionEntity() { + + super(); + } + + @Override + public Long getId() { + + return this.id; + } + + /** + * @param id is the new value of {@link #getId()}. + */ + public void setId(Long id) { + + this.id = id; + } + + /** + * @return the timestamp when this revision has been created. + */ + public long getTimestamp() { + + return this.timestamp; + } + + /** + * @return the {@link #getTimestamp() timestamp} as {@link Date}. + */ + public Date getDate() { + + if (this.date == null) { + this.date = new Date(this.timestamp); + } + return this.date; + } + + /** + * @param timestamp is the new value of {@link #getTimestamp()}. + */ + public void setTimestamp(long timestamp) { + + this.timestamp = timestamp; + } + + /** + * @return the login or id of the user that has created this revision. + */ + + public String getUser() { + + return this.user; + } + + /** + * @param user is the new value of {@link #getUser()}. + */ + public void setUser(String user) { + + this.user = user; + } + + @Override + public int getModificationCounter() { + + return 0; + } + + @Override + public Number getRevision() { + + return null; + } +} \ No newline at end of file diff --git a/modules/jpa-envers/src/main/java/com/devonfw/module/jpa/dataaccess/base/AdvancedRevisionListener.java b/modules/jpa-envers/src/main/java/com/devonfw/module/jpa/dataaccess/base/AdvancedRevisionListener.java new file mode 100644 index 00000000..4c0aa6c6 --- /dev/null +++ b/modules/jpa-envers/src/main/java/com/devonfw/module/jpa/dataaccess/base/AdvancedRevisionListener.java @@ -0,0 +1,35 @@ +/* Copyright (c) The m-m-m Team, Licensed under the Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0 */ +package com.devonfw.module.jpa.dataaccess.base; + +import net.sf.mmm.util.session.api.UserSessionAccess; + +import org.hibernate.envers.RevisionListener; + +/** + * This is the implementation of {@link RevisionListener} that enriches {@link AdvancedRevisionEntity} with additional + * information. + * + * @deprecated If you want to have the backward compatibility with your existing code , please use this class else if + * you are starting the development of application from scratch, please use + * {@link com.devonfw.module.jpa.dataaccess.api.AdvancedRevisionListener} + */ +@Deprecated +public class AdvancedRevisionListener implements RevisionListener { + + /** + * The constructor. + */ + public AdvancedRevisionListener() { + + super(); + } + + @Override + public void newRevision(Object revisionEntity) { + + AdvancedRevisionEntity revision = (AdvancedRevisionEntity) revisionEntity; + revision.setUser(UserSessionAccess.getUserLogin()); + } + +} \ No newline at end of file diff --git a/modules/jpa-spring-data/pom.xml b/modules/jpa-spring-data/pom.xml new file mode 100644 index 00000000..9f1ad76f --- /dev/null +++ b/modules/jpa-spring-data/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-modules + dev-SNAPSHOT + + com.devonfw.java.modules + devon4j-jpa-spring-data + ${devon4j.version} + jar + ${project.artifactId} + Spring-data infrastructure for the Open Application Standard Platform for Java (devon4j). + + 1.8 + + + + + ${project.groupId} + devon4j-jpa-basic + + + org.springframework.data + spring-data-jpa + + + org.aspectj + aspectjweaver + + + org.hibernate + hibernate-envers + true + + + + ${project.groupId} + devon4j-test + test + + + com.h2database + h2 + test + + + + diff --git a/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/api/data/DefaultRepository.java b/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/api/data/DefaultRepository.java new file mode 100644 index 00000000..51922a17 --- /dev/null +++ b/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/api/data/DefaultRepository.java @@ -0,0 +1,13 @@ +package com.devonfw.module.jpa.dataaccess.api.data; + +import net.sf.mmm.util.entity.api.PersistenceEntity; + +/** + * {@link GenericRepository} with defaults applied for simple usage. + * + * @param generic type of the managed {@link PersistenceEntity}. + * @since 3.0.0 + */ +public interface DefaultRepository> extends GenericRepository { + +} diff --git a/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/api/data/DefaultRevisionedRepository.java b/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/api/data/DefaultRevisionedRepository.java new file mode 100644 index 00000000..36a86ebb --- /dev/null +++ b/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/api/data/DefaultRevisionedRepository.java @@ -0,0 +1,14 @@ +package com.devonfw.module.jpa.dataaccess.api.data; + +import net.sf.mmm.util.entity.api.PersistenceEntity; + +/** + * {@link GenericRevisionedRepository} with defaults applied for simple usage. + * + * @param generic type of the managed {@link PersistenceEntity}. + * @since 3.0.0 + */ +public interface DefaultRevisionedRepository> + extends GenericRevisionedRepository { + +} diff --git a/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/api/data/GenericRepository.java b/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/api/data/GenericRepository.java new file mode 100644 index 00000000..67d3b6b4 --- /dev/null +++ b/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/api/data/GenericRepository.java @@ -0,0 +1,73 @@ +package com.devonfw.module.jpa.dataaccess.api.data; + +import static com.querydsl.core.alias.Alias.$; + +import java.io.Serializable; +import java.util.Collection; + +import net.sf.mmm.util.exception.api.ObjectNotFoundUserException; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; + +import com.devonfw.module.jpa.dataaccess.api.QueryUtil; +import com.devonfw.module.jpa.dataaccess.api.feature.FeatureForceIncrementModificationCounter; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.EntityPathBase; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.SimpleExpression; +import com.querydsl.jpa.impl.JPADeleteClause; + +/** + * {@link JpaRepository} with {@link QueryDslSupport} as well as typical Devon4j standard operations. It is recommended + * to use {@link DefaultRepository} instead. + * + * @param generic type of the managed {@link #getEntityClass() entity}. Typically implementing + * {@link net.sf.mmm.util.entity.api.PersistenceEntity}. + * @param generic type of the {@link net.sf.mmm.util.entity.api.PersistenceEntity#getId() primary key} of the + * entity. + * + * @since 3.0.0 + */ +public interface GenericRepository + extends JpaRepository, QueryDslSupport, FeatureForceIncrementModificationCounter { + + /** + * @return the {@link Class} of the managed entity. + */ + Class getEntityClass(); + + /** + * @param id the {@link net.sf.mmm.util.entity.api.PersistenceEntity#getId() primary key}. May not be {@code null}. + * @return the requested entity. Never {@code null}. + * @see #findById(java.io.Serializable) + */ + default E find(ID id) { + + return findById(id).orElseThrow(() -> new ObjectNotFoundUserException(getEntityClass(), id)); + } + + /** + * @param ids the {@link Collection} of {@link net.sf.mmm.util.entity.api.PersistenceEntity#getId() IDs} to delete. + * @return the number of entities that have actually been deleted. + */ + @Modifying + default long deleteByIds(Collection ids) { + + if ((ids == null) || (ids.isEmpty())) { + return 0; + } + E alias = newDslAlias(); + EntityPathBase entityPath = $(alias); + JPADeleteClause delete = newDslDeleteClause(entityPath); + @SuppressWarnings("rawtypes") + Class idType = ids.iterator().next().getClass(); + // https://github.com/querydsl/querydsl/issues/2085 + @SuppressWarnings("unchecked") + SimpleExpression idPath = Expressions.numberPath(idType, entityPath, "id"); + BooleanExpression inClause = QueryUtil.get().newInClause(idPath, ids); + delete.where(inClause); + return delete.execute(); + } + +} diff --git a/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/api/data/GenericRevisionedRepository.java b/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/api/data/GenericRevisionedRepository.java new file mode 100644 index 00000000..6e86e645 --- /dev/null +++ b/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/api/data/GenericRevisionedRepository.java @@ -0,0 +1,62 @@ +package com.devonfw.module.jpa.dataaccess.api.data; + +import java.io.Serializable; +import java.util.List; + +import net.sf.mmm.util.entity.api.RevisionedEntity; + +import org.hibernate.envers.Audited; + +import com.devonfw.module.jpa.dataaccess.api.RevisionMetadata; + +/** + * {@link GenericRepository} with additional support for {@link Audited} + * + * @param generic type of the managed {@link #getEntityClass() entity}. Typically implementing + * {@link net.sf.mmm.util.entity.api.PersistenceEntity} and + * {@link net.sf.mmm.util.entity.api.MutableRevisionedEntity}. + * @param generic type of the {@link net.sf.mmm.util.entity.api.PersistenceEntity#getId() primary key} of the + * entity. + * @since 3.0.0 + */ +public interface GenericRevisionedRepository extends GenericRepository { + + /** + * @param id the {@link net.sf.mmm.util.entity.api.PersistenceEntity#getId() primary key}. + * @param revision the {@link RevisionMetadata#getRevision() revision} of the requested entity. + * @return the entity with the given {@code id} and {@code revision}. + * @see #find(Serializable) + * @see #getRevisionHistoryMetadata(Serializable, boolean) + * @see RevisionMetadata#getRevision() + * @see net.sf.mmm.util.entity.api.RevisionedEntity#getRevision() + */ + E find(ID id, Number revision); + + /** + * @param id the {@link net.sf.mmm.util.entity.api.PersistenceEntity#getId() primary key}. + * @return die {@link List}e der {@link RevisionMetadata Metadaten} der historischen + * {@link RevisionedEntity#getRevision() Revisionen} zur angegebenen Entität. Falls keine Historie existiert, + * wird eine leere {@link List}e zurück geliefert. + */ + default List getRevisionHistoryMetadata(ID id) { + + return getRevisionHistoryMetadata(id, false); + } + + /** + * @param id the {@link net.sf.mmm.util.entity.api.PersistenceEntity#getId() primary key}. + * @param lazy - {@code true} to load the {@link RevisionMetadata} lazily, {@code false} otherwise (eager loading). + * @return the {@link List} of {@link RevisionMetadata} for the historical {@link RevisionedEntity#getRevision() + * revisions} of the {@link net.sf.mmm.util.entity.api.PersistenceEntity} zur angegebenen Entität. Falls keine + * Historie existiert, wird eine leere {@link List}e zurück geliefert. + */ + List getRevisionHistoryMetadata(ID id, boolean lazy); + + /** + * @param id the {@link net.sf.mmm.util.entity.api.PersistenceEntity#getId() primary key}. + * @return die {@link RevisionMetadata Metadaten} der letzten historischen {@link RevisionedEntity#getRevision() + * Revisionen} zur angegebenen Entität. Falls keine Historie existiert, wird {@code null} zurück geliefert. + */ + RevisionMetadata getLastRevisionHistoryMetadata(ID id); + +} diff --git a/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/api/data/QueryDslSupport.java b/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/api/data/QueryDslSupport.java new file mode 100644 index 00000000..97af0ea8 --- /dev/null +++ b/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/api/data/QueryDslSupport.java @@ -0,0 +1,56 @@ +package com.devonfw.module.jpa.dataaccess.api.data; + +import com.querydsl.core.types.EntityPath; +import com.querydsl.jpa.impl.JPADeleteClause; +import com.querydsl.jpa.impl.JPAQuery; + +/** + * Interface for QueryDsl support. All (non-static) methods defined in this interface are considered as internal API of + * a {@link GenericRepository} in order to be used by Java8+ default-method implementations for simplicity. Never call + * such methods from outside the data-access layer. + * + * @param generic type of the entity to query. + * + * @since 3.0.0 + */ +public interface QueryDslSupport { + + /** + * Attention: Please read documentation of {@link QueryDslSupport} before usage. + * + * @return a new {@link JPAQuery}. In most cases you should prefer using {@link #newDslQuery(Object)} instead. + */ + JPAQuery newDslQuery(); + + /** + * Attention: Please read documentation of {@link QueryDslSupport} before usage. + * + * @param alias the {@link #newDslAlias() alias}. + * @return a new {@link JPAQuery} for the given {@link com.querydsl.core.alias.Alias}. + */ + JPAQuery newDslQuery(E alias); + + /** + * Attention: Please read documentation of {@link QueryDslSupport} before usage. + * + * @param alias the {@link #newDslAlias() alias}. + * @return a new {@link JPADeleteClause} for the given {@link com.querydsl.core.alias.Alias}. + */ + JPADeleteClause newDslDeleteClause(E alias); + + /** + * Attention: Please read documentation of {@link QueryDslSupport} before usage. + * + * @param entityPath the {@link EntityPath} to delete from. + * @return a new {@link JPADeleteClause} for the given {@link EntityPath}. + */ + JPADeleteClause newDslDeleteClause(EntityPath entityPath); + + /** + * Attention: Please read documentation of {@link QueryDslSupport} before usage. + * + * @return a new QueryDSL {@link com.querydsl.core.alias.Alias} for the managed entity. + */ + E newDslAlias(); + +} diff --git a/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/impl/data/GenericRepositoryFactoryBean.java b/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/impl/data/GenericRepositoryFactoryBean.java new file mode 100644 index 00000000..09518351 --- /dev/null +++ b/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/impl/data/GenericRepositoryFactoryBean.java @@ -0,0 +1,69 @@ +package com.devonfw.module.jpa.dataaccess.impl.data; + +import java.io.Serializable; + +import javax.persistence.EntityManager; + +import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; +import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; + +import com.devonfw.module.jpa.dataaccess.api.data.GenericRepository; + +/** + * {@link org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport} for + * {@link GenericRepositoryImpl}. In order to use {@link GenericRepository} you need to annotate your spring-boot + * application as following: + * + *
+ * @{@link org.springframework.data.jpa.repository.config.EnableJpaRepositories}(repositoryFactoryBeanClass = {@link GenericRepositoryFactoryBean}.class)
+ * 
+ * + * @param generic type of the {@link GenericRepository} interface. + * @param generic type of the managed {@link net.sf.mmm.util.entity.api.PersistenceEntity entity}. + * @param generic type of the {@link net.sf.mmm.util.entity.api.PersistenceEntity#getId() primary key} of the + * entity. + * + * @since 3.0.0 + */ +public class GenericRepositoryFactoryBean, E, ID extends Serializable> + extends JpaRepositoryFactoryBean { + + /** + * Creates a new {@link JpaRepositoryFactoryBean} for the given repository interface. + * + * @param repositoryInterface must not be {@literal null}. + */ + public GenericRepositoryFactoryBean(Class repositoryInterface) { + + super(repositoryInterface); + } + + @Override + protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) { + + return new GenericRepositoryFactory(entityManager); + } + + private static class GenericRepositoryFactory extends JpaRepositoryFactory { + + /** + * The constructor. + * + * @param entityManager the {@link EntityManager}. + */ + public GenericRepositoryFactory(EntityManager entityManager) { + + super(entityManager); + } + + @Override + protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { + + return GenericRepositoryImpl.class; + } + + } + +} \ No newline at end of file diff --git a/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/impl/data/GenericRepositoryImpl.java b/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/impl/data/GenericRepositoryImpl.java new file mode 100644 index 00000000..80fa3738 --- /dev/null +++ b/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/impl/data/GenericRepositoryImpl.java @@ -0,0 +1,95 @@ +package com.devonfw.module.jpa.dataaccess.impl.data; + +import java.io.Serializable; + +import javax.persistence.EntityManager; +import javax.persistence.LockModeType; + +import org.springframework.data.jpa.repository.support.JpaEntityInformation; +import org.springframework.data.jpa.repository.support.SimpleJpaRepository; + +import com.devonfw.module.jpa.dataaccess.api.data.GenericRepository; +import com.querydsl.core.alias.Alias; +import com.querydsl.core.types.EntityPath; +import com.querydsl.jpa.impl.JPADeleteClause; +import com.querydsl.jpa.impl.JPAQuery; + +/** + * Implementation of {@link GenericRepository} based on {@link SimpleJpaRepository}. All repository interfaces derived + * from {@link GenericRepository} will be based on this implementation at runtime.
+ * Note: We do not use/extend {@link org.springframework.data.jpa.repository.support.QueryDslJpaRepository} as it + * forces you to use QueryDSL APT generation what is not desired. Therefore you will have no support for + * {@link org.springframework.data.querydsl.QueryDslPredicateExecutor}. However, we offer more flexible QueryDSL support + * anyhow. See {@link com.devonfw.module.jpa.dataaccess.api.QueryDslUtil}. + * + * @param generic type of the managed {@link #getEntityClass() entity}. + * @param generic type of the {@link net.sf.mmm.util.entity.api.PersistenceEntity#getId() primary key} of the + * entity. + * + * @since 3.0.0 + */ +public class GenericRepositoryImpl extends SimpleJpaRepository + implements GenericRepository { + + /** The {@link EntityManager} instance. */ + protected final EntityManager entityManager; + + /** The {@link JpaEntityInformation}. */ + protected final JpaEntityInformation entityInformation; + + /** + * The constructor. + * + * @param entityInformation the {@link JpaEntityInformation}. + * @param entityManager the JPA {@link EntityManager}. + */ + public GenericRepositoryImpl(JpaEntityInformation entityInformation, EntityManager entityManager) { + + super(entityInformation, entityManager); + this.entityManager = entityManager; + this.entityInformation = entityInformation; + } + + @Override + public void forceIncrementModificationCounter(E entity) { + + this.entityManager.lock(entity, LockModeType.OPTIMISTIC_FORCE_INCREMENT); + } + + @Override + public Class getEntityClass() { + + return this.entityInformation.getJavaType(); + } + + @Override + public JPAQuery newDslQuery() { + + return new JPAQuery<>(this.entityManager); + } + + @Override + public JPAQuery newDslQuery(E alias) { + + return newDslQuery().from(Alias.$(alias)); + } + + @Override + public JPADeleteClause newDslDeleteClause(E alias) { + + return new JPADeleteClause(this.entityManager, Alias.$(alias)); + } + + @Override + public JPADeleteClause newDslDeleteClause(EntityPath entityPath) { + + return new JPADeleteClause(this.entityManager, entityPath); + } + + @Override + public E newDslAlias() { + + return Alias.alias(this.entityInformation.getJavaType()); + } + +} diff --git a/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/impl/data/GenericRevisionedRepositoryFactoryBean.java b/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/impl/data/GenericRevisionedRepositoryFactoryBean.java new file mode 100644 index 00000000..19092eaf --- /dev/null +++ b/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/impl/data/GenericRevisionedRepositoryFactoryBean.java @@ -0,0 +1,70 @@ +package com.devonfw.module.jpa.dataaccess.impl.data; + +import java.io.Serializable; + +import javax.persistence.EntityManager; + +import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; +import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; + +import com.devonfw.module.jpa.dataaccess.api.data.GenericRepository; +import com.devonfw.module.jpa.dataaccess.api.data.GenericRevisionedRepository; + +/** + * {@link org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport} for + * {@link GenericRepositoryImpl}. In order to use {@link GenericRepository} you need to annotate your spring-boot + * application as following: + * + *
+ * @{@link org.springframework.data.jpa.repository.config.EnableJpaRepositories}(repositoryFactoryBeanClass = {@link GenericRevisionedRepositoryFactoryBean}.class)
+ * 
+ * + * @param generic type of the {@link GenericRevisionedRepository} interface. + * @param generic type of the managed {@link net.sf.mmm.util.entity.api.PersistenceEntity entity}. + * @param generic type of the {@link net.sf.mmm.util.entity.api.PersistenceEntity#getId() primary key} of the + * entity. + * + * @since 3.0.0 + */ +public class GenericRevisionedRepositoryFactoryBean, E, ID extends Serializable> + extends JpaRepositoryFactoryBean { + + /** + * Creates a new {@link JpaRepositoryFactoryBean} for the given repository interface. + * + * @param repositoryInterface must not be {@literal null}. + */ + public GenericRevisionedRepositoryFactoryBean(Class repositoryInterface) { + + super(repositoryInterface); + } + + @Override + protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) { + + return new GenericRevisionedRepositoryFactory(entityManager); + } + + private static class GenericRevisionedRepositoryFactory extends JpaRepositoryFactory { + + /** + * The constructor. + * + * @param entityManager the {@link EntityManager}. + */ + public GenericRevisionedRepositoryFactory(EntityManager entityManager) { + + super(entityManager); + } + + @Override + protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { + + return GenericRevisionedRepositoryImpl.class; + } + + } + +} \ No newline at end of file diff --git a/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/impl/data/GenericRevisionedRepositoryImpl.java b/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/impl/data/GenericRevisionedRepositoryImpl.java new file mode 100644 index 00000000..48134dbc --- /dev/null +++ b/modules/jpa-spring-data/src/main/java/com/devonfw/module/jpa/dataaccess/impl/data/GenericRevisionedRepositoryImpl.java @@ -0,0 +1,106 @@ +package com.devonfw.module.jpa.dataaccess.impl.data; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import javax.persistence.EntityManager; + +import net.sf.mmm.util.entity.api.MutableRevisionedEntity; +import net.sf.mmm.util.exception.api.ObjectNotFoundUserException; + +import org.hibernate.envers.AuditReader; +import org.hibernate.envers.AuditReaderFactory; +import org.springframework.data.jpa.repository.support.JpaEntityInformation; + +import com.devonfw.module.jpa.dataaccess.api.AdvancedRevisionEntity; +import com.devonfw.module.jpa.dataaccess.api.QueryUtil; +import com.devonfw.module.jpa.dataaccess.api.RevisionMetadata; +import com.devonfw.module.jpa.dataaccess.api.RevisionMetadataType; +import com.devonfw.module.jpa.dataaccess.api.data.GenericRevisionedRepository; +import com.devonfw.module.jpa.dataaccess.impl.LazyRevisionMetadata; +import com.querydsl.core.alias.Alias; +import com.querydsl.jpa.impl.JPAQuery; + +/** + * Implementation of {@link GenericRevisionedRepository}. + * + * @param generic type of the managed {@link #getEntityClass() entity}. + * @param generic type of the {@link net.sf.mmm.util.entity.api.PersistenceEntity#getId() primary key} of the + * entity. + * + * @since 3.0.0 + */ +public class GenericRevisionedRepositoryImpl extends GenericRepositoryImpl + implements GenericRevisionedRepository { + + /** + * The constructor. + * + * @param entityInformation the {@link JpaEntityInformation}. + * @param entityManager the JPA {@link EntityManager}. + */ + public GenericRevisionedRepositoryImpl(JpaEntityInformation entityInformation, EntityManager entityManager) { + + super(entityInformation, entityManager); + } + + @Override + public E find(ID id, Number revision) { + + AuditReader auditReader = AuditReaderFactory.get(this.entityManager); + E entity = auditReader.find(this.entityInformation.getJavaType(), id, revision); + if (entity instanceof MutableRevisionedEntity) { + ((MutableRevisionedEntity) entity).setRevision(revision); + } + return entity; + } + + @SuppressWarnings("unchecked") + @Override + public List getRevisionHistoryMetadata(ID id, boolean lazy) { + + AuditReader auditReader = AuditReaderFactory.get(this.entityManager); + List revisionList = auditReader.getRevisions(getEntityClass(), id); + if (revisionList.isEmpty()) { + return Collections.emptyList(); + } + if (lazy) { + List result = new ArrayList<>(revisionList.size()); + for (Number revision : revisionList) { + Long revisionLong = Long.valueOf(revision.longValue()); + result.add(new LazyRevisionMetadata(this.entityManager, revisionLong)); + } + return result; + } else { + AdvancedRevisionEntity rev = Alias.alias(AdvancedRevisionEntity.class); + JPAQuery query = new JPAQuery(this.entityManager) + .from(Alias.$(rev)); + @SuppressWarnings("rawtypes") + List revList = revisionList; + QueryUtil.get().whereIn(query, Alias.$(rev.getId()), (List) revList); + query.orderBy(Alias.$(rev.getId()).asc()); + List resultList = query.fetch(); + return resultList.stream().map(x -> RevisionMetadataType.of(x)).collect(Collectors.toList()); + } + } + + @Override + public RevisionMetadata getLastRevisionHistoryMetadata(ID id) { + + AuditReader auditReader = AuditReaderFactory.get(this.entityManager); + List revisionList = auditReader.getRevisions(getEntityClass(), id); + if (revisionList.isEmpty()) { + return null; + } + Number lastRevision = revisionList.get(revisionList.size() - 1); + AdvancedRevisionEntity revisionEntity = this.entityManager.find(AdvancedRevisionEntity.class, lastRevision); + if (revisionEntity == null) { + throw new ObjectNotFoundUserException(AdvancedRevisionEntity.class, id); + } + return RevisionMetadataType.of(revisionEntity); + } + +} diff --git a/modules/jpa/pom.xml b/modules/jpa/pom.xml new file mode 100644 index 00000000..1e7b0738 --- /dev/null +++ b/modules/jpa/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-modules + dev-SNAPSHOT + + com.devonfw.java.modules + devon4j-jpa + ${devon4j.version} + jar + ${project.artifactId} + JPA-based persistence infrastructure of the Open Application Standard Platform for Java (devon4j). + + + + com.devonfw.java.modules + devon4j-jpa-dao + + + + \ No newline at end of file diff --git a/modules/jpa/src/main/java/com/devonfw/module/jpa/common/api/to/OrderByTo.java b/modules/jpa/src/main/java/com/devonfw/module/jpa/common/api/to/OrderByTo.java new file mode 100644 index 00000000..70fc674c --- /dev/null +++ b/modules/jpa/src/main/java/com/devonfw/module/jpa/common/api/to/OrderByTo.java @@ -0,0 +1,93 @@ +package com.devonfw.module.jpa.common.api.to; + +import com.devonfw.module.basic.common.api.to.AbstractTo; + +/** + * Transfer object to transmit order criteria + * + * @deprecated use org.springframework.data.domain.Sort instead + */ +@Deprecated +public class OrderByTo extends AbstractTo { + + private static final long serialVersionUID = 1L; + + private String name; + + private OrderDirection direction; + + /** + * The constructor. + */ + public OrderByTo() { + + super(); + } + + /** + * The constructor. + * + * @param name the {@link #getName() field name}. + */ + public OrderByTo(String name) { + + this(name, OrderDirection.ASC); + } + + /** + * The constructor. + * + * @param name the {@link #getName() field name}. + * @param direction the {@link #getDirection() sort order direction}. + */ + public OrderByTo(String name, OrderDirection direction) { + + super(); + this.name = name; + this.direction = direction; + } + + /** + * @return the name of the field to order by. + */ + public String getName() { + + return this.name; + } + + /** + * @param name the new value of {@link #getName()}. + */ + public void setName(String name) { + + this.name = name; + } + + /** + * @return the {@link OrderDirection} defining the sort order direction. + */ + public OrderDirection getDirection() { + + if (this.direction == null) { + return OrderDirection.ASC; + } + return this.direction; + } + + /** + * @param direction the new value of {@link #getDirection()}. + */ + public void setDirection(OrderDirection direction) { + + this.direction = direction; + } + + @Override + protected void toString(StringBuilder buffer) { + + buffer.append(this.name); + buffer.append(' '); + buffer.append(this.direction); + } + +} diff --git a/modules/jpa/src/main/java/com/devonfw/module/jpa/common/api/to/OrderDirection.java b/modules/jpa/src/main/java/com/devonfw/module/jpa/common/api/to/OrderDirection.java new file mode 100644 index 00000000..48759238 --- /dev/null +++ b/modules/jpa/src/main/java/com/devonfw/module/jpa/common/api/to/OrderDirection.java @@ -0,0 +1,32 @@ +package com.devonfw.module.jpa.common.api.to; + +/** + * {@link Enum} for sort order. + * + * @deprecated user org.springframework.data.domain.Sort.Direction instead + */ +public enum OrderDirection { + + /** Sort in ascending order. */ + ASC, + + /** Sort in descending order. */ + DESC; + + /** + * @return {@code true}, if {@link OrderDirection#ASC} is set. {@code false} otherwise. + */ + public boolean isAsc() { + + return this == ASC; + } + + /** + * @return {@code true}, if {@link OrderDirection#DESC} is set. {@code false} otherwise. + */ + public boolean isDesc() { + + return this == DESC; + } + +} diff --git a/modules/jpa/src/main/java/com/devonfw/module/jpa/common/api/to/PaginatedListTo.java b/modules/jpa/src/main/java/com/devonfw/module/jpa/common/api/to/PaginatedListTo.java new file mode 100644 index 00000000..9c1677ae --- /dev/null +++ b/modules/jpa/src/main/java/com/devonfw/module/jpa/common/api/to/PaginatedListTo.java @@ -0,0 +1,80 @@ +package com.devonfw.module.jpa.common.api.to; + +import java.util.List; + +import net.sf.mmm.util.entity.api.PersistenceEntity; +import net.sf.mmm.util.transferobject.api.TransferObject; + +import com.devonfw.module.basic.common.api.to.AbstractTo; + +/** + * A paginated list of objects with additional pagination information. + * + * @param is the generic type of the objects. Will usually be a {@link PersistenceEntity persistent entity} when + * used in the data layer, or a {@link TransferObject transfer object}. + * + * @deprecated use org.springframework.data.domain.Page instead. + */ +@Deprecated +public class PaginatedListTo extends AbstractTo { + + /** UID for serialization. */ + private static final long serialVersionUID = 1L; + + /** @see #getPagination() */ + private PaginationResultTo pagination; + + /** @see #getResult() */ + private List result; + + /** + * The constructor. + */ + public PaginatedListTo() { + + super(); + } + + /** + * A convenience constructor which accepts a paginated list and {@link PaginationResultTo pagination information}. + * + * @param result is the list of objects. + * @param pagination is the {@link PaginationResultTo pagination information}. + */ + public PaginatedListTo(List result, PaginationResultTo pagination) { + + this.result = result; + this.pagination = pagination; + } + + /** + * @return the list of objects. + */ + public List getResult() { + + return this.result; + } + + /** + * @return pagination is the {@link PaginationResultTo pagination information}. + */ + public PaginationResultTo getPagination() { + + return this.pagination; + } + + @Override + protected void toString(StringBuilder buffer) { + + super.toString(buffer); + buffer.append('@'); + if (this.result != null) { + buffer.append("#result="); + buffer.append(this.result.size()); + } + if (this.pagination != null) { + buffer.append(','); + this.pagination.toString(buffer); + } + } +} diff --git a/modules/jpa/src/main/java/com/devonfw/module/jpa/common/api/to/PaginationResultTo.java b/modules/jpa/src/main/java/com/devonfw/module/jpa/common/api/to/PaginationResultTo.java new file mode 100644 index 00000000..f9b22b34 --- /dev/null +++ b/modules/jpa/src/main/java/com/devonfw/module/jpa/common/api/to/PaginationResultTo.java @@ -0,0 +1,117 @@ +package com.devonfw.module.jpa.common.api.to; + +import net.sf.mmm.util.exception.api.NlsIllegalArgumentException; + +import com.devonfw.module.basic.common.api.to.AbstractTo; + +/** + * Pagination information about a paginated query. + * + * @deprecated use org.springframework.data.domain.Page instead. + */ +@Deprecated +public class PaginationResultTo extends AbstractTo { + + /** UID for serialization. */ + private static final long serialVersionUID = 1L; + + /** @see #getSize() */ + private Integer size; + + /** @see #getPage() */ + private int page = 1; + + /** @see #getTotal() */ + private Long total; + + /** + * The constructor. + */ + public PaginationResultTo() { + + super(); + } + + /** + * Constructor expecting an existing {@link PaginationTo pagination criteria} and the total number of results found. + * + * @param pagination is an existing {@link PaginationTo pagination criteria}. + * @param total is the total number of results found without pagination. + */ + public PaginationResultTo(PaginationTo pagination, Long total) { + + super(); + + setPage(pagination.getPage()); + setSize(pagination.getSize()); + setTotal(total); + } + + /** + * @return size the size of a page. + */ + public Integer getSize() { + + return this.size; + } + + /** + * @param size the size of a page. + */ + public void setSize(Integer size) { + + this.size = size; + } + + /** + * @return page the current page. + */ + public int getPage() { + + return this.page; + } + + /** + * @param page the current page. Must be greater than 0. + */ + public void setPage(int page) { + + if (page <= 0) { + throw new NlsIllegalArgumentException(page, "page"); + } + this.page = page; + } + + /** + * @return total the total number of entities + */ + public Long getTotal() { + + return this.total; + } + + /** + * @param total the total number of entities + */ + public void setTotal(Long total) { + + this.total = total; + } + + @Override + protected void toString(StringBuilder buffer) { + + super.toString(buffer); + buffer.append("@page="); + buffer.append(this.page); + if (this.size != null) { + buffer.append(", size="); + buffer.append(this.size); + } + if (this.total != null) { + buffer.append(", total="); + buffer.append(this.total); + } + } + +} diff --git a/modules/jpa/src/main/java/com/devonfw/module/jpa/common/api/to/PaginationTo.java b/modules/jpa/src/main/java/com/devonfw/module/jpa/common/api/to/PaginationTo.java new file mode 100644 index 00000000..5972434e --- /dev/null +++ b/modules/jpa/src/main/java/com/devonfw/module/jpa/common/api/to/PaginationTo.java @@ -0,0 +1,99 @@ +package com.devonfw.module.jpa.common.api.to; + +import net.sf.mmm.util.exception.api.NlsIllegalArgumentException; + +import com.devonfw.module.basic.common.api.to.AbstractTo; + +/** + * A {@link net.sf.mmm.util.transferobject.api.TransferObject transfer-object} containing criteria for paginating + * queries. + * + * @deprecated use org.springframework.data.domain.Pageable instead. + */ +@Deprecated +public class PaginationTo extends AbstractTo { + + /** + * Empty {@link PaginationTo} indicating no pagination. + */ + public static final PaginationTo NO_PAGINATION = new PaginationTo(); + + /** UID for serialization. */ + private static final long serialVersionUID = 1L; + + /** @see #getSize() */ + private Integer size; + + /** @see #getPage() */ + private int page = 1; + + /** @see #isTotal() */ + private boolean total; + + /** + * @return size the size of a page. + */ + public Integer getSize() { + + return this.size; + } + + /** + * @param size the size of a page. + */ + public void setSize(Integer size) { + + this.size = size; + } + + /** + * @return page the current page. + */ + public int getPage() { + + return this.page; + } + + /** + * @param page the current page. Must be greater than 0. + */ + public void setPage(int page) { + + if (page <= 0) { + throw new NlsIllegalArgumentException(page, "page"); + } + this.page = page; + } + + /** + * @return total is {@code true} if the client requests that the server calculates the total number of entries found. + */ + public boolean isTotal() { + + return this.total; + } + + /** + * @param total is {@code true} to request calculation of the total number of entries. + */ + public void setTotal(boolean total) { + + this.total = total; + } + + @Override + protected void toString(StringBuilder buffer) { + + super.toString(buffer); + buffer.append("@page="); + buffer.append(this.page); + if (this.size != null) { + buffer.append(", size="); + buffer.append(this.size); + } + if (this.total) { + buffer.append(", total"); + } + } + +} diff --git a/modules/jpa/src/main/java/com/devonfw/module/jpa/common/api/to/SearchCriteriaTo.java b/modules/jpa/src/main/java/com/devonfw/module/jpa/common/api/to/SearchCriteriaTo.java new file mode 100644 index 00000000..c05a1d4c --- /dev/null +++ b/modules/jpa/src/main/java/com/devonfw/module/jpa/common/api/to/SearchCriteriaTo.java @@ -0,0 +1,128 @@ +package com.devonfw.module.jpa.common.api.to; + +import java.util.ArrayList; +import java.util.List; + +import com.devonfw.module.basic.common.api.to.AbstractTo; + +/** + * This is the interface for a {@link net.sf.mmm.util.transferobject.api.TransferObject transfer-object } with the + * criteria for a search and pagination query. Such object specifies the criteria selecting which hits will match when + * performing a search.
+ * NOTE:
+ * This interface only holds the necessary settings for the pagination part of a query. For your individual search, you + * extend {@link SearchCriteriaTo} to create a java bean with all the fields for your search. + * + * @deprecated create your own TO and use org.springframework.data.domain.Pageable for pagination + */ +@Deprecated +public class SearchCriteriaTo extends AbstractTo { + + /** UID for serialization. */ + private static final long serialVersionUID = 1L; + + /** @see #getPagination() */ + private PaginationTo pagination; + + /** @see #getSearchTimeout() */ + private Integer searchTimeout; + + /** @see #getSort() */ + private List sort; + + /** + * The constructor. + */ + public SearchCriteriaTo() { + + super(); + } + + /** + * The currently active pagination. + * + * @return pagination the currently active pagination or {@link PaginationTo#NO_PAGINATION} if no specific pagination + * has been set. Will never return {@code null}. + */ + public PaginationTo getPagination() { + + return this.pagination == null ? PaginationTo.NO_PAGINATION : this.pagination; + } + + /** + * @param pagination the pagination to set + */ + public void setPagination(PaginationTo pagination) { + + this.pagination = pagination; + } + + /** + * Limits the {@link PaginationTo#getSize() page size} by the given limit. + *

+ * If currently no pagination is active, or the {@link PaginationTo#getSize() current page size} is {@code null} or + * greater than the given {@code limit}, the value is replaced by {@code limit} + * + * @param limit is the maximum allowed value for the {@link PaginationTo#getSize() page size}. + */ + public void limitMaximumPageSize(int limit) { + + if (getPagination() == PaginationTo.NO_PAGINATION) { + setPagination(new PaginationTo()); + } + + Integer pageSize = getPagination().getSize(); + if ((pageSize == null) || (pageSize.intValue() > limit)) { + getPagination().setSize((Integer.valueOf(limit))); + } + } + + /** + * This method gets the maximum delay in milliseconds the search may last until it is canceled.
+ * Note:
+ * This feature is the same as the query hint "javax.persistence.query.timeout" in JPA. + * + * @return the search timeout in milliseconds or {@code null} for NO timeout. + */ + public Integer getSearchTimeout() { + + return this.searchTimeout; + } + + /** + * @param searchTimeout is the new value of {@link #getSearchTimeout()}. + */ + public void setSearchTimeout(int searchTimeout) { + + this.searchTimeout = searchTimeout; + } + + /** + * @return sort Sort criterias list + */ + public List getSort() { + + return this.sort; + } + + /** + * @param orderBy the {@link OrderByTo} to add to {@link #getSort() sort}. {@link List} will be created if + * {@code null}. + */ + public void addSort(OrderByTo orderBy) { + + if (this.sort == null) { + this.sort = new ArrayList<>(); + } + this.sort.add(orderBy); + } + + /** + * @param sort Set the sort criterias list + */ + public void setSort(List sort) { + + this.sort = sort; + } + +} diff --git a/modules/jpa/src/main/java/com/devonfw/module/jpa/common/base/LegacyDaoQuerySupport.java b/modules/jpa/src/main/java/com/devonfw/module/jpa/common/base/LegacyDaoQuerySupport.java new file mode 100644 index 00000000..b039db17 --- /dev/null +++ b/modules/jpa/src/main/java/com/devonfw/module/jpa/common/base/LegacyDaoQuerySupport.java @@ -0,0 +1,191 @@ +package com.devonfw.module.jpa.common.base; + +import java.util.List; + +import javax.persistence.Query; + +import net.sf.mmm.util.search.base.AbstractSearchCriteria; + +import com.devonfw.module.jpa.common.api.to.PaginatedListTo; +import com.devonfw.module.jpa.common.api.to.PaginationResultTo; +import com.devonfw.module.jpa.common.api.to.PaginationTo; +import com.devonfw.module.jpa.common.api.to.SearchCriteriaTo; +import com.querydsl.core.types.Expression; +import com.querydsl.jpa.impl.JPAQuery; + +/** + * Upgrade Hint: To gain compatibility to old paging support all you need to do is to navigate to your + * {@code ApplicationDaoImpl} and add {@code LegacyDaoQuerySupport} to the {@code implements} declaration. + * + * @deprecated is only provided for compatibility with legacy features from {@code AbstractGenericDao} for + * {@link SearchCriteriaTo} and paging support. For future usage it is recommended to use the paging API + * from spring-data directly. This module {@code devon4j-jpa} only contains deprecated classes and will not + * be maintained anymore in the future. + * @param type of the entity. + */ +@Deprecated +public interface LegacyDaoQuerySupport { + + /** + * Returns a paginated list of entities according to the supplied {@link SearchCriteriaTo criteria}. + * + * @see #findPaginated(SearchCriteriaTo, JPAQuery, Expression) + * + * @param criteria contains information about the requested page. + * @param query is a query which is preconfigured with the desired conditions for the search. + * @return a paginated list. + */ + default PaginatedListTo findPaginated(SearchCriteriaTo criteria, JPAQuery query) { + + return findPaginated(criteria, query, null); + } + + /** + * Returns a paginated list of entities according to the supplied {@link SearchCriteriaTo criteria}. + *

+ * Applies {@code limit} and {@code offset} values to the supplied {@code query} according to the supplied + * {@link PaginationTo pagination} information inside {@code criteria}. + *

+ * If a {@link PaginationTo#isTotal() total count} of available entities is requested, will also execute a second + * query, without pagination parameters applied, to obtain said count. + *

+ * Will install a query timeout if {@link SearchCriteriaTo#getSearchTimeout()} is not null. + * + * @param criteria contains information about the requested page. + * @param query is a query which is preconfigured with the desired conditions for the search. + * @param expr is used for the final mapping from the SQL result to the entities. + * @return a paginated list. + */ + @SuppressWarnings("unchecked") + default PaginatedListTo findPaginated(SearchCriteriaTo criteria, JPAQuery query, Expression expr) { + + applyCriteria(criteria, query); + PaginationTo pagination = criteria.getPagination(); + PaginationResultTo paginationResult = createPaginationResult(pagination, query); + applyPagination(pagination, query); + JPAQuery finalQuery; + if (expr == null) { + finalQuery = (JPAQuery) query; + } else { + finalQuery = query.select(expr); + } + List paginatedList = finalQuery.fetch(); + return new PaginatedListTo<>(paginatedList, paginationResult); + } + + /** + * Creates a {@link PaginationResultTo pagination result} for the given {@code pagination} and {@code query}. + *

+ * Needs to be called before pagination is applied to the {@code query}. + * + * @param pagination contains information about the requested page. + * @param query is a query preconfigured with the desired conditions for the search. + * @return information about the applied pagination. + */ + default PaginationResultTo createPaginationResult(PaginationTo pagination, JPAQuery query) { + + Long total = calculateTotalBeforePagination(pagination, query); + return new PaginationResultTo(pagination, total); + } + + /** + * Calculates the total number of entities the given {@link JPAQuery query} would return without pagination applied. + *

+ * Needs to be called before pagination is applied to the {@code query}. + * + * @param pagination is the pagination information as requested by the client. + * @param query is the {@link JPAQuery query} for which to calculate the total. + * @return the total count, or {@literal null} if {@link PaginationTo#isTotal()} is {@literal false}. + */ + default Long calculateTotalBeforePagination(PaginationTo pagination, JPAQuery query) { + + Long total = null; + if (pagination.isTotal()) { + total = query.clone().fetchCount(); + } + + return total; + } + + /** + * Applies the {@link PaginationTo pagination criteria} to the given {@link JPAQuery}. + * + * @param pagination is the {@link PaginationTo pagination criteria} to apply. + * @param query is the {@link JPAQuery} to apply to. + */ + default void applyPagination(PaginationTo pagination, JPAQuery query) { + + if (pagination == PaginationTo.NO_PAGINATION) { + return; + } + + Integer limit = pagination.getSize(); + if (limit != null) { + query.limit(limit); + + int page = pagination.getPage(); + if (page > 0) { + query.offset((page - 1) * limit); + } + } + } + + /** + * Applies the meta-data of the given {@link AbstractSearchCriteria search criteria} to the given {@link JPAQuery}. + * + * @param criteria is the {@link AbstractSearchCriteria search criteria} to apply. + * @param query is the {@link JPAQuery} to apply to. + */ + default void applyCriteria(AbstractSearchCriteria criteria, JPAQuery query) { + + Integer limit = criteria.getMaximumHitCount(); + if (limit != null) { + query.limit(limit); + } + int offset = criteria.getHitOffset(); + if (offset > 0) { + query.offset(offset); + } + Long timeout = criteria.getSearchTimeout(); + if (timeout != null) { + query.setHint("javax.persistence.query.timeout", timeout.intValue()); + } + } + + /** + * Applies the meta-data of the given {@link AbstractSearchCriteria search criteria} to the given {@link Query}. + * + * @param criteria is the {@link AbstractSearchCriteria search criteria} to apply. + * @param query is the {@link Query} to apply to. + */ + default void applyCriteria(AbstractSearchCriteria criteria, Query query) { + + Integer limit = criteria.getMaximumHitCount(); + if (limit != null) { + query.setMaxResults(limit); + } + int offset = criteria.getHitOffset(); + if (offset > 0) { + query.setFirstResult(offset); + } + Long timeout = criteria.getSearchTimeout(); + if (timeout != null) { + query.setHint("javax.persistence.query.timeout", timeout.intValue()); + } + } + + /** + * Applies the meta-data of the given {@link SearchCriteriaTo search criteria} to the given {@link Query}. + * + * @param criteria is the {@link AbstractSearchCriteria search criteria} to apply. + * @param query is the {@link JPAQuery} to apply to. + */ + default void applyCriteria(SearchCriteriaTo criteria, JPAQuery query) { + + Integer timeout = criteria.getSearchTimeout(); + if (timeout != null) { + query.setHint("javax.persistence.query.timeout", timeout.intValue()); + } + } + +} diff --git a/modules/jpa/src/test/resources/config/app/common/dozer-mapping.xml b/modules/jpa/src/test/resources/config/app/common/dozer-mapping.xml new file mode 100644 index 00000000..6d44f496 --- /dev/null +++ b/modules/jpa/src/test/resources/config/app/common/dozer-mapping.xml @@ -0,0 +1,31 @@ + + + + + + true + + + java.lang.Long + java.lang.Integer + java.lang.Number + com.devonfw.module.basic.common.api.reference.IdRef + + + + + + com.devonfw.example.general.dataaccess.api.TestApplicationPersistenceEntity + com.devonfw.module.basic.common.api.to.AbstractEto + + this + persistentEntity + + + diff --git a/modules/json/pom.xml b/modules/json/pom.xml new file mode 100644 index 00000000..0264d4ae --- /dev/null +++ b/modules/json/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-modules + dev-SNAPSHOT + + com.devonfw.java.modules + devon4j-json + ${devon4j.version} + jar + ${project.artifactId} + JSON Support Module of the Open Application Standard Platform for Java (devon4j). + + + + com.fasterxml.jackson.jaxrs + jackson-jaxrs-json-provider + + + javax.inject + javax.inject + + + net.sf.m-m-m + mmm-util-core + + + org.springframework + spring-context + + + com.devonfw.java.modules + devon4j-test + test + + + + diff --git a/modules/json/src/main/java/com/devonfw/module/json/common/base/AbstractJsonDeserializer.java b/modules/json/src/main/java/com/devonfw/module/json/common/base/AbstractJsonDeserializer.java new file mode 100644 index 00000000..0dfc49dd --- /dev/null +++ b/modules/json/src/main/java/com/devonfw/module/json/common/base/AbstractJsonDeserializer.java @@ -0,0 +1,128 @@ +package com.devonfw.module.json.common.base; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Calendar; +import java.util.Date; + +import net.sf.mmm.util.date.base.Iso8601UtilImpl; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Helper class to simplify implementation of {@link JsonDeserializer}. + * + * @param the class to be deserialized + * + * @since 3.0.0 + */ +public abstract class AbstractJsonDeserializer extends JsonDeserializer { + + /** + * @param the type to convert the requested value to. + * @param node parent node to deserialize + * @param fieldName the name of the JSON property to get the value from. + * @param type the {@link Class} reflecting the type to convert the requested value to. + * @return requested value converted to the given {@code type}. May not be {@code null}. + * @throws RuntimeException if the requested value is not present or if the conversion failed. + */ + protected V getRequiredValue(JsonNode node, String fieldName, Class type) throws RuntimeException { + + return getValue(node, fieldName, type, true); + } + + /** + * @param the type to convert the requested value to. + * @param node parent node to deserialize + * @param fieldName the name of the JSON property to get the value from. + * @param type the {@link Class} reflecting the type to convert the requested value to. + * @param fallback is the default returned if the requested value is undefined. + * @return requested value converted to the given {@code type} or {@code fallback} if undefined. + * @throws RuntimeException if the conversion failed. + */ + protected V getOptionalValue(JsonNode node, String fieldName, Class type, V fallback) throws RuntimeException { + + V result = getValue(node, fieldName, type, false); + if (result == null) { + result = fallback; + } + return result; + } + + /** + * @param the type to convert the requested value to. + * @param node parent node to deserialize + * @param fieldName the name of the JSON property to get the value from. + * @param type the {@link Class} reflecting the type to convert the requested value to. + * @param required {@code true} if the requested value has to be present, {@code false} otherwise (if optional). + * @return requested value converted to the given {@code type} or {@code null} if undefined and {@code required} is + * {@code true}. + * @throws RuntimeException if {@code required} is {@code true} and the requested value is not present or if the + * conversion failed. + */ + @SuppressWarnings("unchecked") + protected V getValue(JsonNode node, String fieldName, Class type, boolean required) throws RuntimeException { + + V value = null; + JsonNode childNode = node.get(fieldName); + if (childNode != null) { + if (!childNode.isNull()) { + try { + if (type == String.class) { + value = type.cast(childNode.asText()); + } else if (type == BigDecimal.class) { + value = type.cast(new BigDecimal(childNode.asText())); + } else if (type == BigInteger.class) { + value = type.cast(new BigInteger(childNode.asText())); + } else if (type == Date.class) { + value = type.cast(Iso8601UtilImpl.getInstance().parseDate(childNode.asText())); + } else if (type == Calendar.class) { + value = type.cast(Iso8601UtilImpl.getInstance().parseCalendar(childNode.asText())); + } else if (type == Boolean.class) { + // types that may be primitive shall be casted explicitly as Class.cast does not work for primitive types. + value = (V) Boolean.valueOf(childNode.booleanValue()); + } else if (type == Integer.class) { + value = (V) Integer.valueOf(childNode.asText()); + } else if (type == Long.class) { + value = (V) Long.valueOf(childNode.asText()); + } else if (type == Double.class) { + value = (V) Double.valueOf(childNode.asText()); + } else if (type == Float.class) { + value = (V) Float.valueOf(childNode.asText()); + } else if (type == Short.class) { + value = (V) Short.valueOf(childNode.asText()); + } else if (type == Byte.class) { + value = (V) Byte.valueOf(childNode.asText()); + } else { + throw new IllegalArgumentException("Unsupported value type " + type.getName()); + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Failed to convert value to type " + type.getName(), e); + } + } + } + if ((value == null) && (required)) { + throw new IllegalStateException( + "Deserialization failed due to missing " + type.getSimpleName() + " field " + fieldName + "!"); + } + return value; + } + + @Override + public T deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + + JsonNode node = jp.getCodec().readTree(jp); + return deserializeNode(node); + } + + /** + * @param node is the {@link JsonNode} with the value content to be deserialized + * @return the deserialized java object + */ + protected abstract T deserializeNode(JsonNode node); +} diff --git a/modules/json/src/main/java/com/devonfw/module/json/common/base/MixInAnnotationsModule.java b/modules/json/src/main/java/com/devonfw/module/json/common/base/MixInAnnotationsModule.java new file mode 100644 index 00000000..016ba8a3 --- /dev/null +++ b/modules/json/src/main/java/com/devonfw/module/json/common/base/MixInAnnotationsModule.java @@ -0,0 +1,46 @@ +package com.devonfw.module.json.common.base; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeInfo.As; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.module.SimpleModule; + +/** + * A {@link SimpleModule} to extend Jackson to mixin annotations for polymorphic types. + * + * @since 3.0.0 + */ +public class MixInAnnotationsModule extends SimpleModule { + + private static final long serialVersionUID = 1L; + + private final Class[] polymorphicClasses; + + /** + * @param polymorphicClasses the classes reflecting JSON transfer-objects that are polymorphic. + */ + public MixInAnnotationsModule(Class... polymorphicClasses) { + + super("oasp.PolymorphyModule", + new Version(1, 0, 0, null, ObjectMapperFactory.GROUP_ID, ObjectMapperFactory.ARTIFACT_ID)); + this.polymorphicClasses = polymorphicClasses; + } + + @Override + public void setupModule(SetupContext context) { + + for (Class type : this.polymorphicClasses) { + context.setMixInAnnotations(type, JacksonPolymorphicAnnotation.class); + } + } + + /** + * The blueprint class for the following JSON-annotation allowing to convert from JSON to POJO and vice versa + * + */ + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = As.PROPERTY, property = "@type") + public static class JacksonPolymorphicAnnotation { + + } + +} diff --git a/modules/json/src/main/java/com/devonfw/module/json/common/base/ObjectMapperFactory.java b/modules/json/src/main/java/com/devonfw/module/json/common/base/ObjectMapperFactory.java new file mode 100644 index 00000000..7144043f --- /dev/null +++ b/modules/json/src/main/java/com/devonfw/module/json/common/base/ObjectMapperFactory.java @@ -0,0 +1,142 @@ +package com.devonfw.module.json.common.base; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.jsontype.NamedType; +import com.fasterxml.jackson.databind.jsontype.SubtypeResolver; +import com.fasterxml.jackson.databind.module.SimpleModule; + +/** + * A generic factory to {@link #createInstance() create} instances of a Jackson {@link ObjectMapper}. It allows to + * configure the {@link ObjectMapper} for polymorphic transfer-objects. + * + * @see #setBaseClasses(Class...) + * @see #setSubtypes(NamedType...) + * + * @since 3.0.0 + */ +public class ObjectMapperFactory { + + static final String GROUP_ID = "com.devonfw.java"; + + static final String ARTIFACT_ID = "devon4j-rest"; + + private List> baseClassList; + + private List subtypeList; + + private SimpleModule extensionModule; + + /** + * The constructor. + */ + public ObjectMapperFactory() { + + super(); + } + + /** + * Gets access to a generic extension {@link SimpleModule module} for customizations to Jackson JSON mapping. + * + * @see SimpleModule#addSerializer(Class, com.fasterxml.jackson.databind.JsonSerializer) + * @see SimpleModule#addDeserializer(Class, com.fasterxml.jackson.databind.JsonDeserializer) + * + * @return extensionModule + */ + public SimpleModule getExtensionModule() { + + if (this.extensionModule == null) { + this.extensionModule = new SimpleModule("oasp.ExtensionModule", + new Version(1, 0, 0, null, GROUP_ID, ARTIFACT_ID)); + } + return this.extensionModule; + } + + /** + * @param baseClasses are the base classes that are polymorphic (e.g. abstract transfer-object classes that have + * sub-types). You also need to register all sub-types of these polymorphic classes via + * {@link #setSubtypes(NamedType...)}. + */ + public void setBaseClasses(Class... baseClasses) { + + this.baseClassList = Arrays.asList(baseClasses); + } + + /** + * @see #setBaseClasses(Class...) + * + * @param baseClasses the base-classes to add to {@link #setBaseClasses(Class...) base classes list}. + */ + public void addBaseClasses(Class... baseClasses) { + + if (this.baseClassList == null) { + this.baseClassList = new ArrayList<>(); + } + this.baseClassList.addAll(Arrays.asList(baseClasses)); + } + + /** + * @see #setSubtypes(NamedType...) + * + * @param subtypeList the {@link List} of {@link NamedType}s to register the subtypes. + */ + public void setSubtypeList(List subtypeList) { + + this.subtypeList = subtypeList; + } + + /** + * @see #setSubtypes(NamedType...) + * + * @param subtypes the {@link NamedType}s to add to {@link #setSubtypeList(List) sub-type list} for registration. + */ + public void addSubtypes(NamedType... subtypes) { + + if (this.subtypeList == null) { + this.subtypeList = new ArrayList<>(); + } + this.subtypeList.addAll(Arrays.asList(subtypes)); + } + + /** + * @param subtypeList the {@link NamedType}s as pair of {@link Class} reflecting a polymorphic sub-type together with + * its unique name in JSON format. + */ + public void setSubtypes(NamedType... subtypeList) { + + setSubtypeList(Arrays.asList(subtypeList)); + } + + /** + * @return an instance of {@link ObjectMapper} configured for polymorphic resolution. + */ + public ObjectMapper createInstance() { + + ObjectMapper mapper = new ObjectMapper(); + + if ((this.baseClassList != null) && (!this.baseClassList.isEmpty())) { + Class[] baseClasses = this.baseClassList.toArray(new Class[this.baseClassList.size()]); + SimpleModule polymorphyModule = new MixInAnnotationsModule(baseClasses); + mapper.registerModule(polymorphyModule); + } + + if (this.extensionModule != null) { + mapper.registerModule(this.extensionModule); + } + + if (this.subtypeList != null) { + SubtypeResolver subtypeResolver = mapper.getSubtypeResolver(); + for (NamedType subtype : this.subtypeList) { + subtypeResolver.registerSubtypes(subtype); + } + mapper.setSubtypeResolver(subtypeResolver); + } + + return mapper; + } + +} diff --git a/modules/logging/pom.xml b/modules/logging/pom.xml new file mode 100644 index 00000000..2384473c --- /dev/null +++ b/modules/logging/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-modules + dev-SNAPSHOT + + com.devonfw.java.modules + devon4j-logging + ${devon4j.version} + jar + ${project.artifactId} + Logging Module of the Open Application Standard Platform for Java (devon4j). + + + + ch.qos.logback + logback-classic + + + org.slf4j + jcl-over-slf4j + + + org.owasp + security-logging-logback + true + + + javax.servlet + javax.servlet-api + provided + + + org.springframework + spring-web + true + + + javax.inject + javax.inject + + + org.apache.httpcomponents + httpclient + + + com.devonfw.java.modules + devon4j-test + test + + + org.springframework.boot + spring-boot-starter + + + + \ No newline at end of file diff --git a/modules/logging/src/main/java/com/devonfw/module/logging/common/api/DiagnosticContextFacade.java b/modules/logging/src/main/java/com/devonfw/module/logging/common/api/DiagnosticContextFacade.java new file mode 100644 index 00000000..39fb7aa7 --- /dev/null +++ b/modules/logging/src/main/java/com/devonfw/module/logging/common/api/DiagnosticContextFacade.java @@ -0,0 +1,33 @@ +package com.devonfw.module.logging.common.api; + +/** + * This is the interface for a simple facade to write data into the {@link org.slf4j.MDC mapped diagnostic context}. As + * additional value you can easily hook in custom extensions without interfering the logger implementation. A use case + * may be to provide diagnostic informations also to additional components such as a performance monitoring module. + * Therefore setting diagnostic information from Devon4j code is always indirected via this interface so the + * implementation can be extended or replaced (what is not as easy for {@link org.slf4j.MDC#put(String, String) static + * methods}). + * + */ +public interface DiagnosticContextFacade { + + /** + * @return the current {@link LoggingConstants#CORRELATION_ID correlation ID} or {@code null} if not + * {@link #setCorrelationId(String) set}. + */ + String getCorrelationId(); + + /** + * Sets the {@link LoggingConstants#CORRELATION_ID correlation ID} for the current processing and thread. + * + * @param correlationId is the {@link LoggingConstants#CORRELATION_ID correlation ID} as unique identifier for the + * current processing task. + */ + void setCorrelationId(String correlationId); + + /** + * Removes the {@link #setCorrelationId(String) correlation ID} from the diagnostic context. + */ + void removeCorrelationId(); + +} diff --git a/modules/logging/src/main/java/com/devonfw/module/logging/common/api/LoggingConstants.java b/modules/logging/src/main/java/com/devonfw/module/logging/common/api/LoggingConstants.java new file mode 100644 index 00000000..660e1b8e --- /dev/null +++ b/modules/logging/src/main/java/com/devonfw/module/logging/common/api/LoggingConstants.java @@ -0,0 +1,26 @@ +package com.devonfw.module.logging.common.api; + +/** + * Central constants for logging. + * + */ +public final class LoggingConstants { + + /** + * The key for the correlation id used as unique identifier to correlate log entries of a processing task. It allows + * to track down all related log messages for that task across the entire application landscape (e.g. in case of a + * problem). + * + * @see DiagnosticContextFacade#setCorrelationId(String) + */ + public static final String CORRELATION_ID = "correlationId"; + + /** + * Construction prohibited. + */ + private LoggingConstants() { + + super(); + } + +} diff --git a/modules/logging/src/main/java/com/devonfw/module/logging/common/impl/DiagnosticContextFacadeImpl.java b/modules/logging/src/main/java/com/devonfw/module/logging/common/impl/DiagnosticContextFacadeImpl.java new file mode 100644 index 00000000..88f407de --- /dev/null +++ b/modules/logging/src/main/java/com/devonfw/module/logging/common/impl/DiagnosticContextFacadeImpl.java @@ -0,0 +1,40 @@ +package com.devonfw.module.logging.common.impl; + +import org.slf4j.MDC; + +import com.devonfw.module.logging.common.api.DiagnosticContextFacade; +import com.devonfw.module.logging.common.api.LoggingConstants; + +/** + * This is the simple and straight forward implementation of {@link DiagnosticContextFacade}. + * + */ +public class DiagnosticContextFacadeImpl implements DiagnosticContextFacade { + + /** + * The constructor. + */ + public DiagnosticContextFacadeImpl() { + + super(); + } + + @Override + public String getCorrelationId() { + + return MDC.get(LoggingConstants.CORRELATION_ID); + } + + @Override + public void setCorrelationId(String correlationId) { + + MDC.put(LoggingConstants.CORRELATION_ID, correlationId); + } + + @Override + public void removeCorrelationId() { + + MDC.remove(LoggingConstants.CORRELATION_ID); + } + +} diff --git a/modules/logging/src/main/java/com/devonfw/module/logging/common/impl/DiagnosticContextFilter.java b/modules/logging/src/main/java/com/devonfw/module/logging/common/impl/DiagnosticContextFilter.java new file mode 100644 index 00000000..f0495a00 --- /dev/null +++ b/modules/logging/src/main/java/com/devonfw/module/logging/common/impl/DiagnosticContextFilter.java @@ -0,0 +1,159 @@ +package com.devonfw.module.logging.common.impl; + +import java.io.IOException; +import java.util.UUID; + +import javax.inject.Inject; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.context.WebApplicationContext; + +import com.devonfw.module.logging.common.api.DiagnosticContextFacade; + +/** + * Request logging filter that adds the request log message to the SLF4j mapped diagnostic context (MDC) before the + * request is processed, removing it again after the request is processed. + * + */ +public class DiagnosticContextFilter implements Filter { + + private static final Logger LOG = LoggerFactory.getLogger(DiagnosticContextFilter.class); + + /** + * The name of the {@link FilterConfig#getInitParameter(String) init parameter} for + * {@link #setCorrelationIdHttpHeaderName(String)}. + */ + private static final String CORRELATION_ID_HEADER_NAME_PARAM = "correlationIdHeaderName"; + + /** The default value for {@link #setCorrelationIdHttpHeaderName(String)}. */ + public static final String CORRELATION_ID_HEADER_NAME_DEFAULT = "X-Correlation-Id"; + + /** @see #setCorrelationIdHttpHeaderName(String) */ + private String correlationIdHttpHeaderName; + + @Autowired + private WebApplicationContext webApplicationContext; + + private DiagnosticContextFacade diagnosticContextFacade; + + /** + * The constructor. + */ + public DiagnosticContextFilter() { + + super(); + this.correlationIdHttpHeaderName = CORRELATION_ID_HEADER_NAME_DEFAULT; + } + + /** + * @param correlationIdHttpHeaderName is the name of the {@link HttpServletRequest#getHeader(String) HTTP header} for + * the {@link com.devonfw.module.logging.common.api.LoggingConstants#CORRELATION_ID correlation ID}. + */ + public void setCorrelationIdHttpHeaderName(String correlationIdHttpHeaderName) { + + this.correlationIdHttpHeaderName = correlationIdHttpHeaderName; + } + + @Override + public void destroy() { + + } + + private static String normalizeValue(String value) { + + if (value != null) { + String result = value.trim(); + if (!result.isEmpty()) { + return result; + } + } + return null; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + setCorrelationId(request); + try { + chain.doFilter(request, response); + } finally { + this.diagnosticContextFacade.removeCorrelationId(); + } + } + + private void setCorrelationId(ServletRequest request) { + + String correlationId = null; + if (request instanceof HttpServletRequest && this.correlationIdHttpHeaderName != null) { + correlationId = normalizeValue(((HttpServletRequest) request).getHeader(this.correlationIdHttpHeaderName)); + if (correlationId == null) { + LOG.debug("No correlation ID found for HTTP header {}.", this.correlationIdHttpHeaderName); + } else { + this.diagnosticContextFacade.setCorrelationId(correlationId); + LOG.debug("Using correlation ID {} from HTTP header {}.", correlationId, this.correlationIdHttpHeaderName); + return; + } + } + if (correlationId == null) { + // potential fallback if initialized before this filter... + correlationId = normalizeValue(this.diagnosticContextFacade.getCorrelationId()); + if (correlationId != null) { + LOG.debug("Correlation ID was already set to {} before DiagnosticContextFilter has been invoked.", + correlationId); + } else { + // no correlation ID present, create a unique ID + correlationId = UUID.randomUUID().toString(); + this.diagnosticContextFacade.setCorrelationId(correlationId); + LOG.debug("Created unique correlation ID {}.", correlationId); + } + } + } + + /** + * @param diagnosticContextFacade the diagnosticContextFacade to set + */ + @Inject + public void setDiagnosticContextFacade(DiagnosticContextFacade diagnosticContextFacade) { + + this.diagnosticContextFacade = diagnosticContextFacade; + } + + @Override + public void init(FilterConfig config) throws ServletException { + + String headerName = config.getInitParameter(CORRELATION_ID_HEADER_NAME_PARAM); + if (headerName == null) { + LOG.debug("Parameter {} not configured via filter config.", CORRELATION_ID_HEADER_NAME_PARAM); + } else { + this.correlationIdHttpHeaderName = headerName; + } + LOG.info("Correlation ID header initialized to: {}", this.correlationIdHttpHeaderName); + if (this.diagnosticContextFacade == null) { + try { + // ATTENTION: We do not import these classes as we keep spring as an optional dependency. + // If spring is not available in your classpath (e.g. some real JEE context) then this will produce a + // ClassNotFoundException and use the fallback in the catch statement. + ServletContext servletContext = config.getServletContext(); + org.springframework.web.context.WebApplicationContext springContext; + springContext = org.springframework.web.context.support.WebApplicationContextUtils + .getWebApplicationContext(servletContext); + this.diagnosticContextFacade = springContext.getBean(DiagnosticContextFacade.class); + } catch (Throwable e) { + LOG.warn("DiagnosticContextFacade not defined in spring. Falling back to default", e); + this.diagnosticContextFacade = new DiagnosticContextFacadeImpl(); + } + } + } + +} diff --git a/modules/logging/src/main/java/com/devonfw/module/logging/common/impl/PerformanceLogFilter.java b/modules/logging/src/main/java/com/devonfw/module/logging/common/impl/PerformanceLogFilter.java new file mode 100644 index 00000000..05be9de1 --- /dev/null +++ b/modules/logging/src/main/java/com/devonfw/module/logging/common/impl/PerformanceLogFilter.java @@ -0,0 +1,121 @@ +package com.devonfw.module.logging.common.impl; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * Request logging filter that measures the execution time of a request. + * + * @since 1.5.0 + */ +public class PerformanceLogFilter implements Filter { + + private static final Logger LOG = LoggerFactory.getLogger(PerformanceLogFilter.class); + + /** + * Optional filter to only measure execution time of requests that match the filter. + */ + private String urlFilter; + + /** + * The constructor. + */ + public PerformanceLogFilter() { + + super(); + } + + @Override + public void init(FilterConfig config) throws ServletException { + + this.urlFilter = null; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, + ServletException { + + long startTime; + String path = ((HttpServletRequest) request).getServletPath(); + String url = ((HttpServletRequest) request).getRequestURL().toString(); + + if (this.urlFilter == null || path.matches(this.urlFilter)) { + startTime = System.nanoTime(); + try { + chain.doFilter(request, response); + logPerformance(response, startTime, url, null); + } catch (Throwable error) { + logPerformance(response, startTime, url, error); + } + } else { + chain.doFilter(request, response); + } + } + + /** + * Logs the request URL, execution time and {@link HttpStatus}. In case of an error also logs class name and error + * message. + * + * @param response - the {@link ServletResponse} + * @param startTime - start time of the {@link #doFilter(ServletRequest, ServletResponse, FilterChain)} function + * @param url - requested URL + * @param error - error thrown by the requested servlet, {@code null} if execution did not cause an error + */ + private void logPerformance(ServletResponse response, long startTime, String url, Throwable error) { + + long endTime, duration; + int statusCode = ((HttpServletResponse) response).getStatus(); + endTime = System.nanoTime(); + duration = TimeUnit.MILLISECONDS.convert(endTime - startTime, TimeUnit.NANOSECONDS); + + String errorClass = ""; + String errorMessage = ""; + if (error != null) { + statusCode = HttpStatus.SC_INTERNAL_SERVER_ERROR; + errorClass = error.getClass().getName(); + errorMessage = error.getMessage(); + } + String message = + createMessage(url, Long.toString(duration), Integer.toString(statusCode), errorClass, errorMessage); + LOG.info(message); + } + + /** + * Returns a {@link String} representing the log message, which contains the given arguments separated by ';' + * + * @param args - the arguments for the log message + * @return a {@link String} representing the log message + */ + private String createMessage(String... args) { + + StringBuilder buffer = new StringBuilder(); + for (String s : args) { + if (buffer.length() > 0) { + buffer.append(';'); + } + buffer.append(s); + } + return buffer.toString(); + } + + @Override + public void destroy() { + + // nothing to do... + } + +} diff --git a/modules/logging/src/main/java/com/devonfw/module/logging/common/impl/SecureLogging.java b/modules/logging/src/main/java/com/devonfw/module/logging/common/impl/SecureLogging.java new file mode 100644 index 00000000..af13e074 --- /dev/null +++ b/modules/logging/src/main/java/com/devonfw/module/logging/common/impl/SecureLogging.java @@ -0,0 +1,212 @@ +package com.devonfw.module.logging.common.impl; + +import java.lang.reflect.Method; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.Marker; +import org.slf4j.MarkerFactory; + +/** + * Class which provides {@link Marker}s for differential logging. Implements "MultiMarker"s + * ({@link org.owasp.security.logging.MultiMarker}) for optimal filtering if the dependency org.owasp is available, or + * corresponding conventional Markers as a fall back solution. + *

+ * Example usage: + * + *

+ * 
+ * LOG.info({@link SecureLogging}.{@link #SECURITY_FAILURE_CONFIDENTIAL}, "Confidential Security Failure message.");
+ * 
+ * 
+ * + * Example filters for appenders in logback.xml to accept or reject the above log event: + * + *
+ * {@code <}filter class="{@link org.owasp.security.logging.filter.SecurityMarkerFilter}"{@code />}
+ * {@code <}filter class="{@link org.owasp.security.logging.filter.ExcludeClassifiedMarkerFilter}"{@code />}
+ * 
+ */ +public class SecureLogging { + + /** Logger instance. */ + private static final Logger LOG = LoggerFactory.getLogger(SecureLogging.class); + + private static final String EXT_CLASS = "org.owasp.security.logging.SecurityMarkers"; + + private static final String METHOD_NAME = "getMarker"; + + private static boolean initialized = false; + + private static Marker markerSecurSuccConfid = null; + + private static Marker markerSecurFailConfid = null; + + private static Marker markerSecurAuditConfid = null; + + private static final String RESTRICTED_MARKER_NAME = "RESTRICTED"; + + private static final String CONFIDENTIAL_MARKER_NAME = "CONFIDENTIAL"; + + // private static final String SECRET_MARKER_NAME = "SECRET"; // see below. + + private static final String SECURITY_SUCCESS_MARKER_NAME = "SECURITY SUCCESS"; + + private static final String SECURITY_FAILURE_MARKER_NAME = "SECURITY FAILURE"; + + private static final String SECURITY_AUDIT_MARKER_NAME = "SECURITY AUDIT"; + + // MultiMarkers by OWASP do not contain a space between the individual names, so we stick to this behavior. + private static final String SECURITY_SUCCESS_CONFIDENTIAL_MARKER_NAME = "SECURITY SUCCESSCONFIDENTIAL"; + + private static final String SECURITY_FAILURE_CONFIDENTIAL_MARKER_NAME = "SECURITY FAILURECONFIDENTIAL"; + + private static final String SECURITY_AUDIT_CONFIDENTIAL_MARKER_NAME = "SECURITY AUDITCONFIDENTIAL"; + + /** + * Marker for Restricted log events. + */ + public static final Marker RESTRICTED = MarkerFactory.getDetachedMarker(RESTRICTED_MARKER_NAME); + + /** + * Marker for Confidential log events. Usage with OWASP provides possibility for masking, e.g. of passwords. + */ + public static final Marker CONFIDENTIAL = MarkerFactory.getDetachedMarker(CONFIDENTIAL_MARKER_NAME); + + /** + * Marker for Secret log events. This shall not be used until a clear use-case is defined and corresponding measures + * are implemented in the logging chain. By default, secret information shall not be logged. + */ + // public static final Marker SECRET = MarkerFactory.getDetachedMarker(SECRET_MARKER_NAME); + + /** + * Marker for Security Success log events. + */ + public static final Marker SECURITY_SUCCESS = MarkerFactory.getDetachedMarker(SECURITY_SUCCESS_MARKER_NAME); + + /** + * Marker for Security Failure log events. + */ + public static final Marker SECURITY_FAILURE = MarkerFactory.getDetachedMarker(SECURITY_FAILURE_MARKER_NAME); + + /** + * Marker or MultiMarker for Confidential Security Success log events. + */ + public static final Marker SECURITY_SUCCESS_CONFIDENTIAL = getMarkerSecurSuccConfid(); + + /** + * Marker or MultiMarker for Confidential Security Failure log events. + */ + public static final Marker SECURITY_FAILURE_CONFIDENTIAL = getMarkerSecurFailConfid(); + + /** + * Marker or MultiMarker for Confidential Security Audit log events. + */ + public static final Marker SECURITY_AUDIT_CONFIDENTIAL = getMarkerSecurAuditConfid(); + + private SecureLogging() { + } + + private static Marker getMarkerSecurSuccConfid() { + + initMarkers(); + return markerSecurSuccConfid; + } + + private static Marker getMarkerSecurFailConfid() { + + initMarkers(); + return markerSecurFailConfid; + } + + private static Marker getMarkerSecurAuditConfid() { + + initMarkers(); + return markerSecurAuditConfid; + } + + /** + * Main method to initialize the combined {@link Marker}s provided by this class. + */ + private static void initMarkers() { + + if (initialized) + return; + + Class cExtClass = findExtClass(EXT_CLASS); + + if (cExtClass.isAssignableFrom(String.class)) { + createDefaultMarkers(); + } else { + createMultiMarkers(cExtClass); + } + + if (!initialized) + LOG.warn("SecureLogging Markers could not be initialized!"); + else + LOG.debug("SecureLogging Markers created: '{}', ...", markerSecurSuccConfid.getName()); + return; + } + + private static void createDefaultMarkers() { + + LOG.debug("Creating default markers."); + markerSecurSuccConfid = MarkerFactory.getDetachedMarker(SECURITY_SUCCESS_CONFIDENTIAL_MARKER_NAME); + markerSecurFailConfid = MarkerFactory.getDetachedMarker(SECURITY_FAILURE_CONFIDENTIAL_MARKER_NAME); + markerSecurAuditConfid = MarkerFactory.getDetachedMarker(SECURITY_AUDIT_CONFIDENTIAL_MARKER_NAME); + initialized = true; + } + + private static void createMultiMarkers(Class cExtClass) { + + LOG.debug("Creating MultiMarkers."); + + Object objExtClass = null; + try { + objExtClass = cExtClass.newInstance(); + Class[] paramTypes = { Marker[].class }; // the method to invoke is "getMarker(Marker... markers)". + Method method = cExtClass.getMethod(METHOD_NAME, paramTypes); + + Marker[] markerArray = { MarkerFactory.getDetachedMarker(SECURITY_SUCCESS_MARKER_NAME), + MarkerFactory.getDetachedMarker(CONFIDENTIAL_MARKER_NAME) }; + markerSecurSuccConfid = (Marker) method.invoke(objExtClass, (Object) markerArray); + markerArray[0] = MarkerFactory.getDetachedMarker(SECURITY_FAILURE_MARKER_NAME); + markerSecurFailConfid = (Marker) method.invoke(objExtClass, (Object) markerArray); + markerArray[0] = MarkerFactory.getDetachedMarker(SECURITY_AUDIT_MARKER_NAME); + markerSecurAuditConfid = (Marker) method.invoke(objExtClass, (Object) markerArray); + initialized = true; + + } catch (Exception e) { + LOG.warn("Error getting Method '{}' of Class '{}'. Falling back to default.", METHOD_NAME, cExtClass.getName()); + LOG.warn("Exception occurred.", e); + e.printStackTrace(); + createDefaultMarkers(); + } + } + + /** + * @return True if the dependency is available. + */ + public static boolean hasExtClass() { + + Class cExtClass = findExtClass(EXT_CLASS); + return (!cExtClass.isAssignableFrom(String.class)); + } + + /** + * @return The given {@link Class} if parameter 'className' can be resolved, otherwise {@link String}.class. + */ + private static Class findExtClass(String className) { + + Class cExtClass; + try { + cExtClass = Class.forName(className); + return cExtClass; + } catch (Exception e) { + LOG.debug("Class '{}' or one of its dependencies is not present.", className); + cExtClass = String.class; + return cExtClass; + } + } + +} diff --git a/modules/logging/src/main/java/com/devonfw/module/logging/common/impl/SingleLinePatternLayout.java b/modules/logging/src/main/java/com/devonfw/module/logging/common/impl/SingleLinePatternLayout.java new file mode 100644 index 00000000..f043fc3c --- /dev/null +++ b/modules/logging/src/main/java/com/devonfw/module/logging/common/impl/SingleLinePatternLayout.java @@ -0,0 +1,90 @@ +package com.devonfw.module.logging.common.impl; + +import java.util.regex.Pattern; + +import ch.qos.logback.classic.PatternLayout; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.IThrowableProxy; +import ch.qos.logback.classic.spi.StackTraceElementProxy; +import ch.qos.logback.core.CoreConstants; + +/** + * Custom {@link PatternLayout} for logging entries. + * + */ +public class SingleLinePatternLayout extends PatternLayout { + + // --------------- FIELDS --------------- + + /** The separator used as replacement for newlines. */ + private static final String LINE_SEP = " | "; + + /** OS specific line separator. */ + private static final String NEWLINE = CoreConstants.LINE_SEPARATOR; + + /** Regular expression for line breaks. */ + private static final Pattern LINEBREAK_PATTERN = Pattern.compile("[\\r\\n|\\n]"); + + /** Average buffer size per line of stack trace. */ + private static final int BUFFER_PER_LINE = 50; + + // --------------- CONSTRUCTORS --------------- + + /** + * Default constructor. + */ + public SingleLinePatternLayout() { + + super(); + } + + // --------------- METHODS --------------- + + /** + * Creates formatted String, using conversion pattern. + * + * @param event ILoggingEvent + * @return String Formatted event as string in one line + */ + @Override + public String doLayout(ILoggingEvent event) { + + // Format message + String msg = super.doLayout(event).trim(); + // prevent log forging + msg = preventLogForging(msg); + + // Formatting of exception, remove line breaks + IThrowableProxy throwableProxy = event.getThrowableProxy(); + if (throwableProxy != null) { + StackTraceElementProxy[] s = throwableProxy.getStackTraceElementProxyArray(); + if (s != null && s.length > 0) { + // Performance: Initialize StringBuilder with (number of StackTrace-Elements + 1)*50 (+1 for Message) + StringBuilder sb = new StringBuilder(s.length * BUFFER_PER_LINE); + sb.append(msg); + + int len = s.length; + for (int i = 0; i < len; i++) { + sb.append(LINE_SEP).append(s[i]); + } + msg = sb.toString(); + } + } + return msg + NEWLINE; + } + + /** + * Method to prevent log forging. + * + * @param logMsg + * @return Encoded message + */ + private String preventLogForging(String logMsg) { + + String result = logMsg; + // use precompiled pattern for performance reasons + result = LINEBREAK_PATTERN.matcher(logMsg).replaceAll(SingleLinePatternLayout.LINE_SEP); + return result; + } + +} diff --git a/modules/logging/src/main/resources/com/devonfw/logging/logback/appender-console-owasp.xml b/modules/logging/src/main/resources/com/devonfw/logging/logback/appender-console-owasp.xml new file mode 100644 index 00000000..d2365199 --- /dev/null +++ b/modules/logging/src/main/resources/com/devonfw/logging/logback/appender-console-owasp.xml @@ -0,0 +1,43 @@ + + + + + + + + [%marker] ${logPattern} + + + + + + + + + + + + + [%marker] ${logPattern} + + + + + + + + + + if (marker == null) return false; + if (marker.getName().contains("CONFIDENTIAL")) return true; + return false; + + + DENY + + + [%marker] ${logPattern} + + + + diff --git a/modules/logging/src/main/resources/com/devonfw/logging/logback/appender-console.xml b/modules/logging/src/main/resources/com/devonfw/logging/logback/appender-console.xml new file mode 100644 index 00000000..74fa5433 --- /dev/null +++ b/modules/logging/src/main/resources/com/devonfw/logging/logback/appender-console.xml @@ -0,0 +1,8 @@ + + + + + ${logPattern} + + + \ No newline at end of file diff --git a/modules/logging/src/main/resources/com/devonfw/logging/logback/appender-file-debug.xml b/modules/logging/src/main/resources/com/devonfw/logging/logback/appender-file-debug.xml new file mode 100644 index 00000000..beb2dbcb --- /dev/null +++ b/modules/logging/src/main/resources/com/devonfw/logging/logback/appender-file-debug.xml @@ -0,0 +1,15 @@ + + + + ${logPath}/${debugLogFile}.log + + ${logPath}/${debugLogFile}_${rollingPattern}${rollingSuffix}.log + ${rollingAppenderMaxHistory} + + + + ${logPattern} + + + + \ No newline at end of file diff --git a/modules/logging/src/main/resources/com/devonfw/logging/logback/appender-file-info.xml b/modules/logging/src/main/resources/com/devonfw/logging/logback/appender-file-info.xml new file mode 100644 index 00000000..3deb0d32 --- /dev/null +++ b/modules/logging/src/main/resources/com/devonfw/logging/logback/appender-file-info.xml @@ -0,0 +1,18 @@ + + + + ${logPath}/${infoLogFile}.log + + INFO + + + ${logPath}/${infoLogFile}_${rollingPattern}${rollingSuffix}.log + ${rollingAppenderMaxHistory} + + + + ${logPattern} + + + + \ No newline at end of file diff --git a/modules/logging/src/main/resources/com/devonfw/logging/logback/appender-file-owasp.xml b/modules/logging/src/main/resources/com/devonfw/logging/logback/appender-file-owasp.xml new file mode 100644 index 00000000..266f6723 --- /dev/null +++ b/modules/logging/src/main/resources/com/devonfw/logging/logback/appender-file-owasp.xml @@ -0,0 +1,71 @@ + + + + ${logPath}/${debugLogFile}.log + + + ${logPath}/${debugLogFile}_${rollingPattern}${rollingSuffix}.log + ${rollingAppenderMaxHistory} + + + + [%marker] ${logPattern} + + + + + + ${logPath}/${infoLogFile}.log + + + INFO + + + ${logPath}/${infoLogFile}_${rollingPattern}${rollingSuffix}.log + ${rollingAppenderMaxHistory} + + + + [%marker] ${logPattern} + + + + + + ${logPath}/${errorLogFile}.log + + + WARN + + + ${logPath}/${errorLogFile}_${rollingPattern}${rollingSuffix}.log + ${rollingAppenderMaxHistory} + + + + [%marker] ${logPattern} + + + + + + + + + + + + ${logPath}/${securLogFile}.log + + + ${logPath}/${securLogFile}_${rollingPattern}${rollingSuffix}.log + ${rollingAppenderMaxHistory} + + + + [%marker] ${logPattern} + + + + + diff --git a/modules/logging/src/main/resources/com/devonfw/logging/logback/appender-file-warn.xml b/modules/logging/src/main/resources/com/devonfw/logging/logback/appender-file-warn.xml new file mode 100644 index 00000000..7a1d70be --- /dev/null +++ b/modules/logging/src/main/resources/com/devonfw/logging/logback/appender-file-warn.xml @@ -0,0 +1,18 @@ + + + + ${logPath}/${errorLogFile}.log + + WARN + + + ${logPath}/${errorLogFile}_${rollingPattern}${rollingSuffix}.log + ${rollingAppenderMaxHistory} + + + + ${logPattern} + + + + \ No newline at end of file diff --git a/modules/logging/src/main/resources/com/devonfw/logging/logback/appenders-file-all.xml b/modules/logging/src/main/resources/com/devonfw/logging/logback/appenders-file-all.xml new file mode 100644 index 00000000..a5ec57d7 --- /dev/null +++ b/modules/logging/src/main/resources/com/devonfw/logging/logback/appenders-file-all.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/modules/logging/src/main/resources/com/devonfw/logging/logback/appenders-file-default.xml b/modules/logging/src/main/resources/com/devonfw/logging/logback/appenders-file-default.xml new file mode 100644 index 00000000..d222d02d --- /dev/null +++ b/modules/logging/src/main/resources/com/devonfw/logging/logback/appenders-file-default.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/modules/logging/src/main/resources/com/devonfw/logging/logback/application-logging.properties b/modules/logging/src/main/resources/com/devonfw/logging/logback/application-logging.properties new file mode 100644 index 00000000..3b73bbcc --- /dev/null +++ b/modules/logging/src/main/resources/com/devonfw/logging/logback/application-logging.properties @@ -0,0 +1,9 @@ +logPattern=[D: %d{ISO8601}] [P: %-5p] [C: %X{correlationId}] [T: %t] [L: %c] - [M: %m] %n +rollingAppenderMaxHistory=240 +rollingPattern=%d{yyyy-MM-dd_HH} +rollingSuffix=00 +logPath=logs +errorLogFile=error_log_${HOSTNAME}_${appname} +infoLogFile=info_log_${HOSTNAME}_${appname} +debugLogFile=debug_log_${HOSTNAME}_${appname} +securLogFile=secur_log_${HOSTNAME}_${appname} diff --git a/modules/logging/src/test/java/com/devonfw/module/logging/common/impl/DiagnosticContextFilterTest.java b/modules/logging/src/test/java/com/devonfw/module/logging/common/impl/DiagnosticContextFilterTest.java new file mode 100644 index 00000000..bfec33e8 --- /dev/null +++ b/modules/logging/src/test/java/com/devonfw/module/logging/common/impl/DiagnosticContextFilterTest.java @@ -0,0 +1,84 @@ +package com.devonfw.module.logging.common.impl; + +import static org.mockito.Mockito.when; + +import javax.servlet.FilterConfig; + +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.springframework.test.util.ReflectionTestUtils; + +import com.devonfw.module.logging.common.impl.DiagnosticContextFilter; +import com.devonfw.module.test.common.base.ModuleTest; + +/** + * Test of {@link DiagnosticContextFilter}. + */ +public class DiagnosticContextFilterTest extends ModuleTest { + + private static final String CORRELATION_ID_HEADER_NAME_PARAM = "correlationIdHttpHeaderName"; + + private static final String CORRELATION_ID_HEADER_NAME_PARAM_FIELD_NAME = "CORRELATION_ID_HEADER_NAME_PARAM"; + + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + @Mock + private FilterConfig config; + + @Test + public void testCorrelationIdHttpHeaderNameAfterConstructor() { + + // setup + DiagnosticContextFilter filter = new DiagnosticContextFilter(); + + // exercise + String correlationIdHttpHeaderName = (String) ReflectionTestUtils.getField(filter, + CORRELATION_ID_HEADER_NAME_PARAM); + + // verify + assertThat(correlationIdHttpHeaderName).isNotNull(); + } + + @Test + public void testInitWithNullInitParameter() throws Exception { + + // setup + DiagnosticContextFilter filter = new DiagnosticContextFilter(); + String field = (String) ReflectionTestUtils.getField(DiagnosticContextFilter.class, + CORRELATION_ID_HEADER_NAME_PARAM_FIELD_NAME); + assertThat(field).isNotNull(); + when(this.config.getInitParameter(field)).thenReturn(null); + + // exercise + filter.init(this.config); + + // verify + String correlationIdHttpHeaderName = (String) ReflectionTestUtils.getField(filter, + CORRELATION_ID_HEADER_NAME_PARAM); + assertThat(correlationIdHttpHeaderName).isNotNull() + .isEqualTo(DiagnosticContextFilter.CORRELATION_ID_HEADER_NAME_DEFAULT); + } + + @Test + public void testInitWithNonDefaultParameter() throws Exception { + + // setup + DiagnosticContextFilter filter = new DiagnosticContextFilter(); + String field = (String) ReflectionTestUtils.getField(DiagnosticContextFilter.class, + CORRELATION_ID_HEADER_NAME_PARAM_FIELD_NAME); + assertThat(field).isNotNull(); + String nonDefaultParameter = "test"; + when(this.config.getInitParameter(field)).thenReturn(nonDefaultParameter); + + // exercise + filter.init(this.config); + // verify + String correlationIdHttpHeaderName = (String) ReflectionTestUtils.getField(filter, + CORRELATION_ID_HEADER_NAME_PARAM); + assertThat(correlationIdHttpHeaderName).isEqualTo(nonDefaultParameter); + } +} diff --git a/modules/logging/src/test/java/com/devonfw/module/logging/common/impl/SecureLoggingLogbackTest.java b/modules/logging/src/test/java/com/devonfw/module/logging/common/impl/SecureLoggingLogbackTest.java new file mode 100644 index 00000000..8ee1ab9e --- /dev/null +++ b/modules/logging/src/test/java/com/devonfw/module/logging/common/impl/SecureLoggingLogbackTest.java @@ -0,0 +1,255 @@ +package com.devonfw.module.logging.common.impl; + +import static org.mockito.Mockito.verify; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.owasp.security.logging.filter.ExcludeClassifiedMarkerFilter; +import org.owasp.security.logging.mask.MaskingConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.Marker; + +import com.devonfw.module.logging.common.impl.SecureLogging; +import com.devonfw.module.test.common.base.BaseTest; +import com.devonfw.module.test.common.base.ModuleTest; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.PatternLayout; +import ch.qos.logback.classic.encoder.PatternLayoutEncoder; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.core.rolling.RollingFileAppender; +import ch.qos.logback.core.spi.FilterReply; + +/** + * Test class for {@link SecureLogging}, when used with logback.
+ * Tests Marker initialization, logging of events with and without Markers, masking and filtering. + *

+ * Main functionality is adapted from test classes of OWASP:
+ * owasp-security-logging-logback/src/test/java/org/owasp/security/logging/mask/MaskingConverterTest and + * ../filter/ExcludeClassifiedMarkerFilterTest + */ +@RunWith(MockitoJUnitRunner.class) +public class SecureLoggingLogbackTest extends ModuleTest { + + private static final LoggerContext LOGGER_CONTEXT = (LoggerContext) LoggerFactory.getILoggerFactory(); + + private static final Logger LOG = LoggerFactory.getLogger(SecureLoggingLogbackTest.class); + + private PatternLayoutEncoder encoder; + + private ExcludeClassifiedMarkerFilter filterExclClassif; + + @Mock + private RollingFileAppender mockAppender = new RollingFileAppender<>(); + + // Captor is genericised with ch.qos.logback.classic.spi.LoggingEvent + @Captor + private ArgumentCaptor captorLoggingEvent; + + /** + * {@inheritDoc} + *

+ * Called by {@code final} method {@link BaseTest#setUp()}. + */ + @Override + protected void doSetUp() { + + super.doSetUp(); + + // This converter masks all arguments of a confidential message with ***. + // It overwrites the message field %m, so the log pattern can stay unchanged. + PatternLayout.defaultConverterMap.put("m", MaskingConverter.class.getName()); + + this.encoder = new PatternLayoutEncoder(); + this.encoder.setContext(LOGGER_CONTEXT); + this.encoder.setPattern("[%marker] %-4relative [%thread] %-5level %logger{35} - %m%n"); + this.encoder.start(); + + this.filterExclClassif = new ExcludeClassifiedMarkerFilter(); + this.filterExclClassif.setContext(LOGGER_CONTEXT); + this.filterExclClassif.start(); + assertThat(this.filterExclClassif.isStarted()).isTrue(); + + this.mockAppender.setContext(LOGGER_CONTEXT); + this.mockAppender.setEncoder(this.encoder); + this.mockAppender.start(); + + ((ch.qos.logback.classic.Logger) LOG).addAppender(this.mockAppender); + } + + /** + * {@inheritDoc} + *

+ * Called by {@code final} method {@link BaseTest#tearDown()}. + */ + @Override + protected void doTearDown() { + + super.doTearDown(); + + ((ch.qos.logback.classic.Logger) LOG).detachAppender(this.mockAppender); + } + + private LoggingEvent getLastLogEvent() { + + // Verify our logging interactions + verify(this.mockAppender).doAppend(this.captorLoggingEvent.capture()); + // Get the logging event from the captor + return this.captorLoggingEvent.getValue(); + } + + /** + * Test if logging works at all, without using any Markers. + */ + @Test + public void testDefaultLogEvent() { + + // given + String logmsg = "simple log message"; + + // when + LOG.info(logmsg); + + // then + // Retrieve log event + final LoggingEvent loggingEvent = getLastLogEvent(); + // Check log level is correct + assertThat(loggingEvent.getLevel()).isEqualTo(Level.INFO); + + // Check the message being logged is reasonable + String layoutMessage = this.encoder.getLayout().doLayout(loggingEvent); + assertThat(layoutMessage.isEmpty()).as("formatted log message is empty.").isFalse(); + assertThat(layoutMessage.contains(logmsg)).as("formatted log message contains original message.").isTrue(); + } + + /** + * Test the output of a log event with a marker. + */ + @Test + public void testLogEventWithMarker() { + + // given + Marker marker = SecureLogging.SECURITY_SUCCESS; + String logmsg = "security log message"; + + // when + LOG.info(marker, logmsg); + + // then + // Retrieve log event + final LoggingEvent loggingEvent = getLastLogEvent(); + // Check log level is correct + assertThat(loggingEvent.getLevel()).isEqualTo(Level.INFO); + + // Check the message being logged is reasonable + String layoutMessage = this.encoder.getLayout().doLayout(loggingEvent); + assertThat(layoutMessage.contains(logmsg)).as("formatted log message contains original message.").isTrue(); + assertThat(layoutMessage.contains(marker.getName())).as("log message contains name of marker.").isTrue(); + } + + /** + * Test the output of a log event with a classification Marker and an argument that shall be masked. Note: the console + * will show the 'password' content when running this test. + */ + @Test + public void testLogEventWithMasking() { + + // given + Marker marker = SecureLogging.CONFIDENTIAL; + String password = "classified!"; + + // when + LOG.info(marker, "confidential message with password = '{}'", password); + + // then + // Retrieve log event + final LoggingEvent loggingEvent = getLastLogEvent(); + // Check log level is correct + assertThat(loggingEvent.getLevel()).isEqualTo(Level.INFO); + + // Check the message being logged is reasonable + String layoutMessage = this.encoder.getLayout().doLayout(loggingEvent); + assertThat(layoutMessage.contains(password)).as("formatted log message contains classified information.").isFalse(); + assertThat(layoutMessage.contains(marker.getName())).as("log message contains name of marker.").isTrue(); + } + + /** + * Test the ExcludeClassifiedMarkerFilter on an event with MultiMarker (other tests are done within OWASP). + */ + @Test + public void testExclClassifMarkerFilter() { + + // given + Marker marker = SecureLogging.SECURITY_SUCCESS_CONFIDENTIAL; + + // when + LOG.info(marker, "confidential security message with MultiMarker."); + + // then + // Retrieve log event + final LoggingEvent loggingEvent = getLastLogEvent(); + // Check log level is correct + assertThat(loggingEvent.getLevel()).isEqualTo(Level.INFO); + + // Check the stand-alone filter decision for this event + assertThat(this.filterExclClassif.decide(loggingEvent)).isEqualTo(FilterReply.DENY); + + // Check the filter chain decision for this event + // (does not work) assertThat(this.mockAppender.getFilterChainDecision(loggingEvent)).isEqualTo(FilterReply.DENY); + } + + /** + * Test if a combined Marker contains the names of its constituent Markers. This test is useful in particular if the + * dependency org.owasp is not available, but also works when it is present. To test the fall back solution in + * {@link SecureLogging}, one has to create a separate logging module strictly without the OWASP dependency. + */ + @Test + public void testInitMarkersByName() { + + // given & when + // setup: SecureLogging.initMarkers() is called by the loc below. + Marker multiMarker = SecureLogging.SECURITY_SUCCESS_CONFIDENTIAL; + Marker securMarker = SecureLogging.SECURITY_SUCCESS; + Marker confidMarker = SecureLogging.CONFIDENTIAL; + + // then + // verify that the combined Marker or MultiMarker contains the names of its constituent Markers. + assertThat(multiMarker.getName().isEmpty()).isFalse(); + assertThat(multiMarker.getName().contains(securMarker.getName())).isTrue(); + assertThat(multiMarker.getName().contains(confidMarker.getName())).isTrue(); + } + + /** + * Test Marker creation if the dependency org.owasp is available, which provides the class + * {@link org.owasp.security.logging.MultiMarker}. + */ + @Test + public void testInitWithMultiMarkerClass() { + + // skip test if the dependency is not available. + if (!SecureLogging.hasExtClass()) { + return; + } + assertThat(SecureLogging.hasExtClass()).as("dependency org.owasp is available.").isTrue(); + + // given & when + // SecureLogging.initMarkers() is called by the loc below: + Marker multiMarker = SecureLogging.SECURITY_SUCCESS_CONFIDENTIAL; + Marker securMarker = SecureLogging.SECURITY_SUCCESS; + Marker confidMarker = SecureLogging.CONFIDENTIAL; + + // then + // verify that the MultiMarker contains both simple Markers. + assertThat(multiMarker.hasReferences()).as("MultiMarker has references.").isTrue(); + assertThat(multiMarker.contains(securMarker)).as("MultiMarker contains Security Marker.").isTrue(); + assertThat(multiMarker.contains(confidMarker)).as("MultiMarker contains Confidential Marker.").isTrue(); + } + +} diff --git a/modules/pom.xml b/modules/pom.xml new file mode 100644 index 00000000..8e460915 --- /dev/null +++ b/modules/pom.xml @@ -0,0 +1,114 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j + dev-SNAPSHOT + + devon4j-modules + pom + ${project.artifactId} + Reusable modules of the Open Application Standard Platform for Java (devon4j). + + + logging + test + configuration + beanmapping + service + json + rest + cxf-client + cxf-client-rest + cxf-client-ws + cxf-server + cxf-server-rest + cxf-server-ws + security + jpa + jpa-basic + jpa-dao + jpa-envers + jpa-spring-data + test-jpa + batch + web + basic + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + + com.devonfw.java.boms + devon4j-bom + ${devon4j.version} + pom + import + + + + + + + org.slf4j + slf4j-api + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + javax.interceptor + javax.interceptor-api + 1.2 + + + + private + ${project.reporting.outputEncoding} + ${project.build.sourceEncoding} + true + ${user.dir}/src/main/javadoc/stylesheet.css + + + http://docs.oracle.com/javase/7/docs/api/ + http://oasp.github.io/devon4j/maven/apidocs/ + + JavaDocs for ${project.name} + JavaDocs for ${project.name} + + + + devon4j.javadoc + + javadoc + + + + devon4j.javadoc.aggregate + + aggregate + + + + + + + + diff --git a/modules/rest/pom.xml b/modules/rest/pom.xml new file mode 100644 index 00000000..1498c427 --- /dev/null +++ b/modules/rest/pom.xml @@ -0,0 +1,82 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-modules + dev-SNAPSHOT + + com.devonfw.java.modules + devon4j-rest + ${devon4j.version} + jar + ${project.artifactId} + REST-Service Support Module of the Open Application Standard Platform for Java (devon4j). + + + + com.fasterxml.jackson.jaxrs + jackson-jaxrs-json-provider + + + javax.ws.rs + javax.ws.rs-api + + + ${project.groupId} + devon4j-service + + + ${project.groupId} + devon4j-json + + + javax.inject + javax.inject + + + javax.validation + validation-api + + + org.springframework + spring-context + + + net.sf.m-m-m + mmm-util-validation + + + org.springframework.security + spring-security-core + test + + + + org.glassfish.jersey.core + jersey-common + 2.4.1 + test + + + com.devonfw.java.modules + devon4j-test + test + + + org.hibernate.validator + hibernate-validator + test + + + javax.el + javax.el-api + + + org.glassfish.web + javax.el + + + + diff --git a/modules/rest/src/main/java/com/devonfw/module/rest/common/api/RestService.java b/modules/rest/src/main/java/com/devonfw/module/rest/common/api/RestService.java new file mode 100644 index 00000000..eb7b0154 --- /dev/null +++ b/modules/rest/src/main/java/com/devonfw/module/rest/common/api/RestService.java @@ -0,0 +1,20 @@ +package com.devonfw.module.rest.common.api; + +import javax.ws.rs.Consumes; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import com.devonfw.module.service.common.api.Service; + +/** + * This is a marker interface for a REST {@link Service}. It is recommended to extend your REST API interfaces from this + * {@link RestService} interface to make your life easier. However, you are not forced to do so. See JavaDoc of + * {@link Service} for further details. + * + * @since 3.0.0 + */ +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public interface RestService extends Service { + +} diff --git a/modules/rest/src/main/java/com/devonfw/module/rest/service/api/RequestParameters.java b/modules/rest/src/main/java/com/devonfw/module/rest/service/api/RequestParameters.java new file mode 100644 index 00000000..44af8a48 --- /dev/null +++ b/modules/rest/src/main/java/com/devonfw/module/rest/service/api/RequestParameters.java @@ -0,0 +1,213 @@ +package com.devonfw.module.rest.service.api; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import javax.ws.rs.BadRequestException; +import javax.ws.rs.InternalServerErrorException; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.UriInfo; + +/** + * This class helps to deal with {@link UriInfo} and {@link MultivaluedMap} from the JAX-RS API. E.g. if you have a REST + * query operation for a collection URI you can use {@link UriInfo} in case you want to support a mixture of optional + * and required parameters. The methods provided here throw according exceptions such as {@link BadRequestException} and + * already support conversion of values. + * + * @since 2.0.0 + */ +public class RequestParameters { + + private final MultivaluedMap parameters; + + /** + * The constructor. + * + * @param parameters is the {@link MultivaluedMap} containing the parameters to wrap. + */ + public RequestParameters(MultivaluedMap parameters) { + + super(); + this.parameters = parameters; + } + + /** + * Gets the single parameter in a generic and flexible way. + * + * @param is the generic type of targetType. + * @param key is the {@link java.util.Map#get(Object) key} of the parameter to get. + * @param targetType is the {@link Class} reflecting the type to convert the value to. Supports common Java standard + * types such as {@link String}, {@link Long}, {@link Double}, {@link BigDecimal}, etc. + * @param required - {@code true} if the value is required and a {@link BadRequestException} is thrown if it is not + * present, {@code false} otherwise (if optional). + * @return the value for the given key converted to the given targetType. May be + * {@code null} if required is {@code false} . + * @throws WebApplicationException if an error occurred. E.g. {@link BadRequestException} if a required parameter is + * missing or {@link InternalServerErrorException} if the given targetType is not supported. + */ + @SuppressWarnings("unchecked") + public T get(String key, Class targetType, boolean required) throws WebApplicationException { + + String value = get(key); + if (value == null) { + if (required) { + throw new BadRequestException("Missing parameter: " + key); + } + Object result = null; + if (targetType.isPrimitive()) { + if (targetType == boolean.class) { + result = Boolean.FALSE; + } else if (targetType == int.class) { + result = Integer.valueOf(0); + } else if (targetType == long.class) { + result = Long.valueOf(0); + } else if (targetType == double.class) { + result = Double.valueOf(0); + } else if (targetType == float.class) { + result = Float.valueOf(0); + } else if (targetType == byte.class) { + result = Byte.valueOf((byte) 0); + } else if (targetType == short.class) { + result = Short.valueOf((short) 0); + } else if (targetType == char.class) { + result = '\0'; + } + } + return (T) result; + } + try { + return convertValue(value, targetType); + } catch (WebApplicationException e) { + throw e; + } catch (Exception e) { + throw new BadRequestException("Failed to convert '" + value + "' to type " + targetType); + } + } + + /** + * Converts the given value to the given targetType. + * + * @param is the generic type of targetType. + * @param value is the value to convert. + * @param targetType is the {@link Class} reflecting the type to convert the value to. + * @return the converted value. + * @throws ParseException if parsing of the given value failed while converting. + */ + @SuppressWarnings("unchecked") + protected T convertValue(String value, Class targetType) throws ParseException { + + if (value == null) { + return null; + } + Object result; + if (targetType == String.class) { + result = value; + } else if (targetType.isEnum()) { + for (T instance : targetType.getEnumConstants()) { + Enum e = (Enum) instance; + if (e.name().equalsIgnoreCase(value)) { + return instance; + } + } + throw new IllegalArgumentException("Enum constant not found!"); + } else if ((targetType == boolean.class) || (targetType == Boolean.class)) { + result = Boolean.parseBoolean(value); + } else if ((targetType == int.class) || (targetType == Integer.class)) { + result = Integer.valueOf(value); + } else if ((targetType == long.class) || (targetType == Long.class)) { + result = Long.valueOf(value); + } else if ((targetType == double.class) || (targetType == Double.class)) { + result = Double.valueOf(value); + } else if ((targetType == float.class) || (targetType == Float.class)) { + result = Float.valueOf(value); + } else if ((targetType == short.class) || (targetType == Short.class)) { + result = Short.valueOf(value); + } else if ((targetType == byte.class) || (targetType == Byte.class)) { + result = Byte.valueOf(value); + } else if (targetType == BigDecimal.class) { + result = new BigDecimal(value); + } else if (targetType == BigInteger.class) { + result = new BigInteger(value); + } else if (targetType == Date.class) { + result = new SimpleDateFormat("YYYY-MM-dd'T'HH:mm:ss").parseObject(value); + } else { + throw new InternalServerErrorException("Unsupported type " + targetType); + } + // do not use type.cast() as not working for primitive types. + return (T) result; + } + + /** + * Gets the parameter as single value with the given key as {@link String}. + * + * @param key is the {@link java.util.Map#get(Object) key} of the parameter to get. + * @return the requested parameter. Will be {@code null} if the parameter is not present. + * @throws BadRequestException if the parameter is defined multiple times (see {@link #getList(String)}). + */ + public String get(String key) throws BadRequestException { + + List list = this.parameters.get(key); + if ((list == null) || (list.isEmpty())) { + return null; + } + if (list.size() > 1) { + throw new BadRequestException("Duplicate parameter: " + key); + } + return list.get(0); + } + + /** + * Gets the parameter with the given key as {@link String}. Unlike {@link #get(String)} this method will + * not throw an exception if the parameter is multi-valued but just return the first value. + * + * @param key is the {@link java.util.Map#get(Object) key} of the parameter to get. + * @return the first value of the requested parameter. Will be {@code null} if the parameter is not present. + */ + public String getFirst(String key) { + + return this.parameters.getFirst(key); + } + + /** + * Gets the {@link List} of all value for the parameter with with the given key. In general you should + * avoid multi-valued parameters (e.g. http://host/path?query=a&query=b). The JAX-RS API supports this exotic case as + * first citizen so we expose it here but only use it if you know exactly what you are doing. + * + * @param key is the {@link java.util.Map#get(Object) key} of the parameter to get. + * @return the {@link List} with all values of the requested parameter. Will be an {@link Collections#emptyList() + * empty list} if the parameter is not present. + */ + public List getList(String key) { + + List list = this.parameters.get(key); + if (list == null) { + list = Collections.emptyList(); + } + return list; + } + + /** + * @param uriInfo is the {@link UriInfo}. + * @return a new instance of {@link RequestParameters} for {@link UriInfo#getQueryParameters()}. + */ + public static RequestParameters fromQuery(UriInfo uriInfo) { + + return new RequestParameters(uriInfo.getQueryParameters()); + } + + /** + * @param uriInfo is the {@link UriInfo}. + * @return a new instance of {@link RequestParameters} for {@link UriInfo#getPathParameters()}. + */ + public static RequestParameters fromPath(UriInfo uriInfo) { + + return new RequestParameters(uriInfo.getPathParameters()); + } + +} diff --git a/modules/rest/src/main/java/com/devonfw/module/rest/service/impl/RestServiceExceptionFacade.java b/modules/rest/src/main/java/com/devonfw/module/rest/service/impl/RestServiceExceptionFacade.java new file mode 100644 index 00000000..d4d06d46 --- /dev/null +++ b/modules/rest/src/main/java/com/devonfw/module/rest/service/impl/RestServiceExceptionFacade.java @@ -0,0 +1,574 @@ +package com.devonfw.module.rest.service.impl; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import javax.inject.Inject; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Path.Node; +import javax.validation.ValidationException; +import javax.ws.rs.ClientErrorException; +import javax.ws.rs.ServerErrorException; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +import net.sf.mmm.util.exception.api.NlsRuntimeException; +import net.sf.mmm.util.exception.api.NlsThrowable; +import net.sf.mmm.util.exception.api.TechnicalErrorUserException; +import net.sf.mmm.util.exception.api.ValidationErrorUserException; +import net.sf.mmm.util.lang.api.StringUtil; +import net.sf.mmm.util.security.api.SecurityErrorUserException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.devonfw.module.service.common.api.constants.ServiceConstants; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * This is an implementation of {@link ExceptionMapper} that acts as generic exception facade for REST services. It + * {@link #toResponse(Throwable) maps} {@link Throwable exceptions} to an according HTTP status code and JSON result as + * defined by OASP REST error + * specification. + * + * @since 2.0.0 + */ +@Provider +public class RestServiceExceptionFacade implements ExceptionMapper { + + /** + * JSON key for {@link Throwable#getMessage() error message}. + * + * @deprecated use {@link ServiceConstants#KEY_MESSAGE}. + */ + @Deprecated + public static final String KEY_MESSAGE = ServiceConstants.KEY_MESSAGE; + + /** + * JSON key for {@link NlsRuntimeException#getUuid() error ID}. + * + * @deprecated use {@link ServiceConstants#KEY_UUID}. + */ + @Deprecated + public static final String KEY_UUID = ServiceConstants.KEY_UUID; + + /** + * JSON key for {@link NlsRuntimeException#getCode() error code}. + * + * @deprecated use {@link ServiceConstants#KEY_CODE}. + */ + @Deprecated + public static final String KEY_CODE = ServiceConstants.KEY_CODE; + + /** + * JSON key for (validation) errors. + * + * @deprecated use {@link ServiceConstants#KEY_ERRORS}. + */ + @Deprecated + public static final String KEY_ERRORS = ServiceConstants.KEY_ERRORS; + + /** Logger instance. */ + private static final Logger LOG = LoggerFactory.getLogger(RestServiceExceptionFacade.class); + + private final List> securityExceptions; + + private final Class transactionSystemException; + + private final Class rollbackException; + + private ObjectMapper mapper; + + private boolean exposeInternalErrorDetails; + + /** + * The constructor. + */ + public RestServiceExceptionFacade() { + + super(); + this.securityExceptions = new ArrayList<>(); + registerToplevelSecurityExceptions(); + this.transactionSystemException = loadException("org.springframework.transaction.TransactionSystemException"); + this.rollbackException = loadException("javax.persistence.RollbackException"); + } + + /** + * Registers a {@link Class} as a top-level security {@link Throwable exception}. Instances of this class and all its + * subclasses will be handled as security errors. Therefore an according HTTP error code is used and no further + * details about the exception is send to the client to prevent + * sensitive data exposure. + * + * @param securityException is the {@link Class} reflecting the security error. + */ + protected void registerToplevelSecurityException(Class securityException) { + + this.securityExceptions.add(securityException); + } + + /** + * This method registers the {@link #registerToplevelSecurityException(Class) top-level security exceptions}. You may + * override it to add additional or other classes. + */ + protected void registerToplevelSecurityExceptions() { + + this.securityExceptions.add(SecurityException.class); + this.securityExceptions.add(SecurityErrorUserException.class); + registerToplevelSecurityExceptions("org.springframework.security.access.AccessDeniedException"); + registerToplevelSecurityExceptions("org.springframework.security.authentication.AuthenticationServiceException"); + registerToplevelSecurityExceptions( + "org.springframework.security.authentication.AuthenticationCredentialsNotFoundException"); + registerToplevelSecurityExceptions("org.springframework.security.authentication.BadCredentialsException"); + registerToplevelSecurityExceptions("org.springframework.security.authentication.AccountExpiredException"); + } + + /** + * @param className the className to be registered + */ + protected void registerToplevelSecurityExceptions(String className) { + + Class securityException = loadException(className); + if (securityException != null) { + registerToplevelSecurityException(securityException); + } + } + + private Class loadException(String className) { + + try { + @SuppressWarnings("unchecked") + Class exception = (Class) Class.forName(className); + return exception; + } catch (ClassNotFoundException e) { + LOG.info("Exception {} was not found on classpath and can not be handled by this {}.", className, + getClass().getSimpleName()); + } catch (Exception e) { + LOG.error("Exception {} is invalid and can not be handled by this {}.", className, getClass().getSimpleName(), e); + } + return null; + } + + @Override + public Response toResponse(Throwable exception) { + + if (exception instanceof WebApplicationException) { + return createResponse((WebApplicationException) exception); + } else if (exception instanceof NlsRuntimeException) { + return toResponse(exception, exception); + } else { + Throwable error = exception; + Throwable catched = exception; + error = getRollbackCause(exception); + if (error == null) { + error = unwrapNlsUserError(exception); + } + if (error == null) { + error = exception; + } + return toResponse(error, catched); + } + } + + /** + * Unwraps potential NLS user error from a wrapper exception such as {@code JsonMappingException} or + * {@code PersistenceException}. + * + * @param exception the exception to unwrap. + * @return the unwrapped {@link NlsRuntimeException} exception or {@code null} if no + * {@link NlsRuntimeException#isForUser() use error}. + */ + private NlsRuntimeException unwrapNlsUserError(Throwable exception) { + + Throwable cause = exception.getCause(); + if (cause instanceof NlsRuntimeException) { + NlsRuntimeException nlsError = (NlsRuntimeException) cause; + if (nlsError.isForUser()) { + return nlsError; + } + } + return null; + } + + private Throwable getRollbackCause(Throwable exception) { + + Class exceptionClass = exception.getClass(); + if (exceptionClass == this.transactionSystemException) { + Throwable cause = exception.getCause(); + if (cause != null) { + exceptionClass = cause.getClass(); + if (exceptionClass == this.rollbackException) { + return cause.getCause(); + } + } + } + return null; + } + + /** + * @see #toResponse(Throwable) + * + * @param exception the exception to handle + * @param catched the original exception that was cached. Either same as {@code error} or a (child-) + * {@link Throwable#getCause() cause} of it. + * @return the response build from the exception. + */ + protected Response toResponse(Throwable exception, Throwable catched) { + + if (exception instanceof ValidationException) { + return handleValidationException(exception, catched); + } else if (exception instanceof ValidationErrorUserException) { + return createResponse(exception, (ValidationErrorUserException) exception, null); + } else { + Class exceptionClass = exception.getClass(); + for (Class securityError : this.securityExceptions) { + if (securityError.isAssignableFrom(exceptionClass)) { + return handleSecurityError(exception, catched); + } + } + return handleGenericError(exception, catched); + } + } + + /** + * Creates the {@link Response} for the given validation exception. + * + * @param exception is the original validation exception. + * @param error is the wrapped exception or the same as exception. + * @param errorsMap is a map with all validation errors + * @return the requested {@link Response}. + */ + protected Response createResponse(Throwable exception, ValidationErrorUserException error, + Map> errorsMap) { + + LOG.warn("Service failed due to validation failure.", error); + if (exception == error) { + return createResponse(Status.BAD_REQUEST, error, errorsMap); + } else { + return createResponse(Status.BAD_REQUEST, error, exception.getMessage(), errorsMap); + } + } + + /** + * Exception handling for generic exception (fallback). + * + * @param exception the exception to handle + * @param catched the original exception that was cached. Either same as {@code error} or a (child-) + * {@link Throwable#getCause() cause} of it. + * @return the response build from the exception + */ + protected Response handleGenericError(Throwable exception, Throwable catched) { + + NlsRuntimeException userError; + boolean logged = false; + if (exception instanceof NlsThrowable) { + NlsThrowable nlsError = (NlsThrowable) exception; + if (!nlsError.isTechnical()) { + LOG.warn("Service failed due to business error: {}", nlsError.getMessage()); + logged = true; + } + userError = TechnicalErrorUserException.getOrCreateUserException(exception); + } else { + userError = TechnicalErrorUserException.getOrCreateUserException(catched); + } + if (!logged) { + LOG.error("Service failed on server", userError); + } + return createResponse(userError); + } + + /** + * Exception handling for security exception. + * + * @param exception the exception to handle + * @param catched the original exception that was cached. Either same as {@code error} or a (child-) + * {@link Throwable#getCause() cause} of it. + * @return the response build from exception + */ + protected Response handleSecurityError(Throwable exception, Throwable catched) { + + NlsRuntimeException error; + if ((exception == catched) && (exception instanceof NlsRuntimeException)) { + error = (NlsRuntimeException) exception; + } else { + error = new SecurityErrorUserException(catched); + } + LOG.error("Service failed due to security error", error); + // NOTE: for security reasons we do not send any details about the error to the client! + String message; + String code = null; + if (this.exposeInternalErrorDetails) { + message = getExposedErrorDetails(error); + } else { + message = "forbidden"; + } + return createResponse(Status.FORBIDDEN, message, code, error.getUuid(), null); + } + + /** + * Exception handling for validation exception. + * + * @param exception the exception to handle + * @param catched the original exception that was cached. Either same as {@code error} or a (child-) + * {@link Throwable#getCause() cause} of it. + * @return the response build from the exception. + */ + protected Response handleValidationException(Throwable exception, Throwable catched) { + + Throwable t = catched; + Map> errorsMap = null; + if (exception instanceof ConstraintViolationException) { + ConstraintViolationException constraintViolationException = (ConstraintViolationException) exception; + Set> violations = constraintViolationException.getConstraintViolations(); + errorsMap = new HashMap<>(); + + for (ConstraintViolation violation : violations) { + Iterator it = violation.getPropertyPath().iterator(); + String fieldName = null; + + // Getting fieldname from the exception + while (it.hasNext()) { + fieldName = it.next().toString(); + } + + List errorsList = errorsMap.get(fieldName); + + if (errorsList == null) { + errorsList = new ArrayList<>(); + errorsMap.put(fieldName, errorsList); + } + + errorsList.add(violation.getMessage()); + + } + + t = new ValidationException(errorsMap.toString(), catched); + } + ValidationErrorUserException error = new ValidationErrorUserException(t); + return createResponse(t, error, errorsMap); + } + + /** + * @param error is the {@link Throwable} to extract message details from. + * @return the exposed message(s). + */ + protected String getExposedErrorDetails(Throwable error) { + + StringBuilder buffer = new StringBuilder(); + Throwable e = error; + while (e != null) { + if (buffer.length() > 0) { + buffer.append(StringUtil.LINE_SEPARATOR); + } + buffer.append(e.getClass().getSimpleName()); + buffer.append(": "); + buffer.append(e.getLocalizedMessage()); + e = e.getCause(); + } + return buffer.toString(); + } + + /** + * Create the {@link Response} for the given {@link NlsRuntimeException}. + * + * @param error the generic {@link NlsRuntimeException}. + * @return the corresponding {@link Response}. + */ + protected Response createResponse(NlsRuntimeException error) { + + Status status; + if (error.isTechnical()) { + status = Status.INTERNAL_SERVER_ERROR; + } else { + status = Status.BAD_REQUEST; + } + return createResponse(status, error, null); + } + + /** + * Create a response message as a JSON-String from the given parts. + * + * @param status is the HTTP {@link Status}. + * @param error is the catched or wrapped {@link NlsRuntimeException}. + * @param errorsMap is a map with all validation errors + * @return the corresponding {@link Response}. + */ + protected Response createResponse(Status status, NlsRuntimeException error, Map> errorsMap) { + + String message; + if (this.exposeInternalErrorDetails) { + message = getExposedErrorDetails(error); + } else { + message = error.getLocalizedMessage(); + } + return createResponse(status, error, message, errorsMap); + } + + /** + * Create a response message as a JSON-String from the given parts. + * + * @param status is the HTTP {@link Status}. + * @param error is the catched or wrapped {@link NlsRuntimeException}. + * @param message is the JSON message attribute. + * @param errorsMap is a map with all validation errors + * @return the corresponding {@link Response}. + */ + protected Response createResponse(Status status, NlsRuntimeException error, String message, + Map> errorsMap) { + + return createResponse(status, error, message, error.getCode(), errorsMap); + } + + /** + * Create a response message as a JSON-String from the given parts. + * + * @param status is the HTTP {@link Status}. + * @param error is the catched or wrapped {@link NlsRuntimeException}. + * @param message is the JSON message attribute. + * @param code is the {@link NlsRuntimeException#getCode() error code}. + * @param errorsMap is a map with all validation errors + * @return the corresponding {@link Response}. + */ + protected Response createResponse(Status status, NlsRuntimeException error, String message, String code, + Map> errorsMap) { + + return createResponse(status, message, code, error.getUuid(), errorsMap); + } + + /** + * Create a response message as a JSON-String from the given parts. + * + * @param status is the HTTP {@link Status}. + * @param message is the JSON message attribute. + * @param code is the {@link NlsRuntimeException#getCode() error code}. + * @param uuid the {@link UUID} of the response message. + * @param errorsMap is a map with all validation errors + * @return the corresponding {@link Response}. + */ + protected Response createResponse(Status status, String message, String code, UUID uuid, + Map> errorsMap) { + + String json = createJsonErrorResponseMessage(message, code, uuid, errorsMap); + return Response.status(status).entity(json).build(); + } + + /** + * Create a response message as a JSON-String from the given parts. + * + * @param message the message of the response message + * @param code the code of the response message + * @param uuid the uuid of the response message + * @param errorsMap is a map with all validation errors + * @return the response message as a JSON-String + */ + protected String createJsonErrorResponseMessage(String message, String code, UUID uuid, + Map> errorsMap) { + + Map jsonMap = new HashMap<>(); + if (message != null) { + jsonMap.put(ServiceConstants.KEY_MESSAGE, message); + } + if (code != null) { + jsonMap.put(ServiceConstants.KEY_CODE, code); + } + if (uuid != null) { + jsonMap.put(ServiceConstants.KEY_UUID, uuid.toString()); + } + + if (errorsMap != null) { + jsonMap.put(ServiceConstants.KEY_ERRORS, errorsMap); + } + + String responseMessage = ""; + try { + responseMessage = this.mapper.writeValueAsString(jsonMap); + } catch (JsonProcessingException e) { + LOG.error("Exception facade failed to create JSON.", e); + responseMessage = "{}"; + } + return responseMessage; + + } + + /** + * Add a response message to an existing response. + * + * @param exception the {@link WebApplicationException}. + * @return the response with the response message added + */ + protected Response createResponse(WebApplicationException exception) { + + Response response = exception.getResponse(); + int statusCode = response.getStatus(); + Status status = Status.fromStatusCode(statusCode); + NlsRuntimeException error; + if (exception instanceof ServerErrorException) { + error = new TechnicalErrorUserException(exception); + LOG.error("Service failed on server", error); + return createResponse(status, error, null); + } else { + UUID uuid = UUID.randomUUID(); + if (exception instanceof ClientErrorException) { + LOG.warn("Service failed due to unexpected request. UUDI: {}, reason: {} ", uuid, exception.getMessage()); + } else { + LOG.warn("Service caused redirect or other error. UUID: {}, reason: {}", uuid, exception.getMessage()); + } + return createResponse(status, exception.getMessage(), String.valueOf(statusCode), uuid, null); + } + + } + + /** + * @return the {@link ObjectMapper} for JSON mapping. + */ + public ObjectMapper getMapper() { + + return this.mapper; + } + + /** + * @param mapper the mapper to set + */ + @Inject + public void setMapper(ObjectMapper mapper) { + + this.mapper = mapper; + } + + /** + * @param exposeInternalErrorDetails - {@code true} if internal exception details shall be exposed to clients (useful + * for debugging and testing), {@code false} if such details are hidden to prevent + * Sensitive Data Exposure + * (default, has to be used in production environment). + */ + public void setExposeInternalErrorDetails(boolean exposeInternalErrorDetails) { + + this.exposeInternalErrorDetails = exposeInternalErrorDetails; + if (exposeInternalErrorDetails) { + String message = + "****** Exposing of internal error details is enabled! This violates OWASP A6 (Sensitive Data Exposure) and shall only be used for testing/debugging and never in production. ******"; + LOG.warn(message); + // CHECKSTYLE:OFF (for development only) + System.err.println(message); + // CHECKSTYLE:ON + } + } + + /** + * @return exposeInternalErrorDetails the value set by {@link #setExposeInternalErrorDetails(boolean)}. + */ + public boolean isExposeInternalErrorDetails() { + + return this.exposeInternalErrorDetails; + } + +} diff --git a/modules/rest/src/main/java/com/devonfw/module/rest/service/impl/json/AbstractJsonDeserializer.java b/modules/rest/src/main/java/com/devonfw/module/rest/service/impl/json/AbstractJsonDeserializer.java new file mode 100644 index 00000000..86104ff0 --- /dev/null +++ b/modules/rest/src/main/java/com/devonfw/module/rest/service/impl/json/AbstractJsonDeserializer.java @@ -0,0 +1,14 @@ +package com.devonfw.module.rest.service.impl.json; + +import com.fasterxml.jackson.databind.JsonDeserializer; + +/** + * Helper class to simplify implementation of {@link JsonDeserializer}. + * + * @param the class to be deserialized + * @deprecated use {@link com.devonfw.module.json.common.base.AbstractJsonDeserializer} instead. + */ +@Deprecated +public abstract class AbstractJsonDeserializer extends com.devonfw.module.json.common.base.AbstractJsonDeserializer { + +} diff --git a/modules/rest/src/main/java/com/devonfw/module/rest/service/impl/json/ObjectMapperFactory.java b/modules/rest/src/main/java/com/devonfw/module/rest/service/impl/json/ObjectMapperFactory.java new file mode 100644 index 00000000..5c6c55cf --- /dev/null +++ b/modules/rest/src/main/java/com/devonfw/module/rest/service/impl/json/ObjectMapperFactory.java @@ -0,0 +1,25 @@ +package com.devonfw.module.rest.service.impl.json; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.jsontype.NamedType; + +/** + * A generic factory to {@link #createInstance() create} instances of a Jackson {@link ObjectMapper}. It allows to + * configure the {@link ObjectMapper} for polymorphic transfer-objects. + * + * @see #setBaseClasses(Class...) + * @see #setSubtypes(NamedType...) + * @deprecated use {@link com.devonfw.module.json.common.base.ObjectMapperFactory} instead. + */ +@Deprecated +public class ObjectMapperFactory extends com.devonfw.module.json.common.base.ObjectMapperFactory { + + /** + * The constructor. + */ + public ObjectMapperFactory() { + + super(); + } + +} diff --git a/modules/rest/src/test/java/com/devonfw/module/rest/service/impl/RestServiceExceptionFacadeTest.java b/modules/rest/src/test/java/com/devonfw/module/rest/service/impl/RestServiceExceptionFacadeTest.java new file mode 100644 index 00000000..b9dbfa9e --- /dev/null +++ b/modules/rest/src/test/java/com/devonfw/module/rest/service/impl/RestServiceExceptionFacadeTest.java @@ -0,0 +1,335 @@ +package com.devonfw.module.rest.service.impl; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Validation; +import javax.validation.ValidationException; +import javax.validation.Validator; +import javax.validation.constraints.Min; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.InternalServerErrorException; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.core.Response; + +import net.sf.mmm.util.exception.api.IllegalCaseException; +import net.sf.mmm.util.exception.api.NlsRuntimeException; +import net.sf.mmm.util.exception.api.ObjectNotFoundUserException; +import net.sf.mmm.util.exception.api.TechnicalErrorUserException; +import net.sf.mmm.util.lang.api.StringUtil; +import net.sf.mmm.util.security.api.SecurityErrorUserException; +import net.sf.mmm.util.validation.api.ValidationErrorUserException; + +import org.junit.Test; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.AccountExpiredException; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.InternalAuthenticationServiceException; + +import com.devonfw.module.rest.service.impl.RestServiceExceptionFacade; +import com.devonfw.module.test.common.base.ModuleTest; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Test-case for {@link RestServiceExceptionFacade}. + * + */ +public class RestServiceExceptionFacadeTest extends ModuleTest { + + /** Value of {@link TechnicalErrorUserException#getCode()}. */ + private static final String CODE_TECHNICAL_ERROR = "TechnicalError"; + + /** Placeholder for any UUID. */ + private static final String UUID_ANY = ""; + + /** + * @return the {@link RestServiceExceptionFacade} instance to test. + */ + protected RestServiceExceptionFacade getExceptionFacade() { + + RestServiceExceptionFacade facade = new RestServiceExceptionFacade(); + facade.setMapper(new ObjectMapper()); + return facade; + } + + /** + * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with constraint violations + */ + @Test + public void testConstraintViolationExceptions() { + + class CounterTest { + + @Min(value = 10) + private Integer count; + + public CounterTest(Integer count) { + + this.count = count; + } + + } + + CounterTest counter = new CounterTest(new Integer(1)); + + Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + Set> violations = validator.validate(counter); + + RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); + String message = "{count=[" + violations.iterator().next().getMessage() + "]}"; + String errors = "{count=[" + violations.iterator().next().getMessage() + "]}"; + Throwable error = new ConstraintViolationException(violations); + checkFacade(exceptionFacade, error, 400, message, UUID_ANY, ValidationErrorUserException.CODE, errors); + + } + + /** + * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with forbidden security exception including + * subclasses. + */ + @Test + public void testSecurityExceptions() { + + RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); + + String secretMessage = "Secret information not to be revealed on client - only to be logged on server!"; + + int statusCode = 403; + String message = "forbidden"; + String code = null; + + checkFacade(exceptionFacade, new AccessDeniedException(secretMessage), statusCode, message, UUID_ANY, code); + checkFacade(exceptionFacade, new AuthenticationCredentialsNotFoundException(secretMessage), statusCode, message, + UUID_ANY, code, null); + checkFacade(exceptionFacade, new BadCredentialsException(secretMessage), statusCode, message, UUID_ANY, code); + checkFacade(exceptionFacade, new AccountExpiredException(secretMessage), statusCode, message, UUID_ANY, code); + checkFacade(exceptionFacade, new InternalAuthenticationServiceException(secretMessage), statusCode, message, + UUID_ANY, code, null); + SecurityErrorUserException error = new SecurityErrorUserException(); + checkFacade(exceptionFacade, error, statusCode, message, error.getUuid().toString(), code); + } + + /** + * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with forbidden security exception including + * subclasses. + */ + @Test + public void testSecurityExceptionExposed() { + + RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); + exceptionFacade.setExposeInternalErrorDetails(true); + + String secretMessage = "Secret information not to be revealed on client - only to be logged on server!"; + + int statusCode = 403; + String message = + "The operation failed due to security restrictions. Please contact the support in case of a permission problem."; + String code = null; + + checkFacade(exceptionFacade, new AccessDeniedException(secretMessage), statusCode, "SecurityErrorUserException: " + + message + StringUtil.LINE_SEPARATOR + "AccessDeniedException: " + secretMessage, UUID_ANY, code); + } + + /** + * Checks that the specified {@link RestServiceExceptionFacade} provides the expected results for the given + * {@link Throwable}. + * + * @param exceptionFacade is the {@link RestServiceExceptionFacade} to test. + * @param error is the {@link Throwable} to convert. + * @param statusCode is the expected {@link Response#getStatus() status} code. + * @param message is the expected {@link Throwable#getMessage() error message} from the JSON result. + * @param uuid is the expected {@link NlsRuntimeException#getUuid() UUID} from the JSON result. May be + * {@code null}. + * @param code is the expected {@link NlsRuntimeException#getCode() error code} from the JSON result. May be + * {@code null}. + * @return the JSON result for potential further asserts. + */ + protected String checkFacade(RestServiceExceptionFacade exceptionFacade, Throwable error, int statusCode, + String message, String uuid, String code) { + + return checkFacade(exceptionFacade, error, statusCode, message, uuid, code, null); + } + + /** + * Checks that the specified {@link RestServiceExceptionFacade} provides the expected results for the given + * {@link Throwable}. + * + * @param exceptionFacade is the {@link RestServiceExceptionFacade} to test. + * @param error is the {@link Throwable} to convert. + * @param statusCode is the expected {@link Response#getStatus() status} code. + * @param message is the expected {@link Throwable#getMessage() error message} from the JSON result. + * @param uuid is the expected {@link NlsRuntimeException#getUuid() UUID} from the JSON result. May be + * {@code null}. + * @param code is the expected {@link NlsRuntimeException#getCode() error code} from the JSON result. May be + * {@code null}. + * @param errors is the expected validation errors in a format key-value + * @return the JSON result for potential further asserts. + */ + @SuppressWarnings("unchecked") + protected String checkFacade(RestServiceExceptionFacade exceptionFacade, Throwable error, int statusCode, + String message, String uuid, String code, String errors) { + + Response response = exceptionFacade.toResponse(error); + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(statusCode); + + Object entity = response.getEntity(); + assertThat(entity).isInstanceOf(String.class); + String result = (String) entity; + + try { + Map valueMap = exceptionFacade.getMapper().readValue(result, Map.class); + String msg = message; + if (msg == null) { + msg = error.getLocalizedMessage(); + } + assertThat(valueMap.get(RestServiceExceptionFacade.KEY_MESSAGE)).isEqualTo(msg); + if ((statusCode == 403) && (!exceptionFacade.isExposeInternalErrorDetails())) { + assertThat(result).doesNotContain(error.getMessage()); + } + assertThat(valueMap.get(RestServiceExceptionFacade.KEY_CODE)).isEqualTo(code); + String actualUuid = (String) valueMap.get(RestServiceExceptionFacade.KEY_UUID); + if (UUID_ANY.equals(uuid)) { + if (actualUuid == null) { + fail("UUID expected but not found in response: " + result); + } + } else { + assertThat(actualUuid).isEqualTo(uuid); + } + + Map> errorsMap = + (Map>) valueMap.get(RestServiceExceptionFacade.KEY_ERRORS); + + if (errors == null) { + if (errorsMap != null) { + fail("Errors do not expected but found in response: " + result); + } else { + assertThat(errorsMap).isEqualTo(errors); + } + } else { + if (errorsMap != null) { + assertThat(errorsMap.toString()).isEqualTo(errors); + } else { + fail("Errors expected but not found in response: " + result); + } + + } + + } catch (Exception e) { + throw new IllegalStateException(e.getMessage(), e); + } + return result; + } + + /** + * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception including + * subclasses. + */ + @Test + public void testJaxrsInternalServerException() { + + RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); + String internalMessage = "The HTTP request is invalid"; + int statusCode = 500; + InternalServerErrorException error = new InternalServerErrorException(internalMessage); + String expectedMessage = new TechnicalErrorUserException(error).getLocalizedMessage(); + checkFacade(exceptionFacade, error, statusCode, expectedMessage, UUID_ANY, CODE_TECHNICAL_ERROR); + } + + /** + * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception. + */ + @Test + public void testJaxrsBadRequestException() { + + RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); + String message = "The HTTP request is invalid"; + Throwable error = new BadRequestException(message); + checkFacade(exceptionFacade, error, 400, message, UUID_ANY, "400"); + } + + /** + * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with a {@link ValidationException}. + */ + @Test + public void testValidationException() { + + RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); + String message = "Validation failed!"; + Throwable error = new ValidationException(message); + checkFacade(exceptionFacade, error, 400, message, UUID_ANY, ValidationErrorUserException.CODE); + } + + /** + * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception including + * subclasses. + */ + @Test + public void testJaxrsNotFoundException() { + + RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); + String internalMessage = "Either the service URL is wrong or the requested resource does not exist"; + checkFacade(exceptionFacade, new NotFoundException(internalMessage), 404, internalMessage, UUID_ANY, "404"); + } + + /** + * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception including + * subclasses. + */ + @Test + public void testTechnicalJavaRuntimeServerException() { + + RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); + String secretMessage = "Internal server error occurred"; + IllegalArgumentException error = new IllegalArgumentException(secretMessage); + String expectedMessage = new TechnicalErrorUserException(error).getLocalizedMessage(); + checkFacade(exceptionFacade, error, 500, expectedMessage, UUID_ANY, CODE_TECHNICAL_ERROR); + } + + /** + * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception including + * subclasses. + */ + @Test + public void testTechnicalCustomRuntimeServerException() { + + RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); + String message = "Internal server error occurred"; + IllegalCaseException error = new IllegalCaseException(message); + String expectedMessage = new TechnicalErrorUserException(error).getLocalizedMessage(); + checkFacade(exceptionFacade, error, 500, expectedMessage, error.getUuid().toString(), CODE_TECHNICAL_ERROR); + } + + /** + * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception including + * subclasses. + */ + @Test + public void testTechnicalCustomRuntimeServerExceptionExposed() { + + RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); + exceptionFacade.setExposeInternalErrorDetails(true); + String message = "Internal server error occurred"; + IllegalCaseException error = new IllegalCaseException(message); + String expectedMessage = + "TechnicalErrorUserException: An unexpected error has occurred! We apologize any inconvenience. Please try again later." + + StringUtil.LINE_SEPARATOR + error.getClass().getSimpleName() + ": " + error.getLocalizedMessage(); + checkFacade(exceptionFacade, error, 500, expectedMessage, error.getUuid().toString(), CODE_TECHNICAL_ERROR); + } + + /** + * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception including + * subclasses. + */ + @Test + public void testBusinessException() { + + RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); + ObjectNotFoundUserException error = new ObjectNotFoundUserException(4711L); + checkFacade(exceptionFacade, error, 400, null, error.getUuid().toString(), "NotFound"); + } +} diff --git a/modules/security/pom.xml b/modules/security/pom.xml new file mode 100644 index 00000000..3b919b72 --- /dev/null +++ b/modules/security/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-modules + dev-SNAPSHOT + + com.devonfw.java.modules + devon4j-security + ${devon4j.version} + jar + ${project.artifactId} + Security Module of the Open Application Standard Platform for Java (devon4j). + + + + + + org.springframework + spring-web + + + javax.servlet + javax.servlet-api + provided + + + + + org.springframework.security + spring-security-config + + + org.springframework.security + spring-security-web + + + + + javax.inject + javax.inject + + + javax.annotation + javax.annotation-api + + + + + com.fasterxml.jackson.core + jackson-databind + + + + + com.devonfw.java.modules + devon4j-test + test + + + ${project.groupId} + devon4j-logging + test + + + com.google.guava + guava + test + + + \ No newline at end of file diff --git a/modules/security/src/main/java/com/devonfw/module/security/common/api/accesscontrol/AccessControl.java b/modules/security/src/main/java/com/devonfw/module/security/common/api/accesscontrol/AccessControl.java new file mode 100644 index 00000000..dc0b30d7 --- /dev/null +++ b/modules/security/src/main/java/com/devonfw/module/security/common/api/accesscontrol/AccessControl.java @@ -0,0 +1,100 @@ +package com.devonfw.module.security.common.api.accesscontrol; + +import java.io.Serializable; +import java.util.Objects; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlID; +import javax.xml.bind.annotation.XmlSchemaType; +import javax.xml.bind.annotation.adapters.CollapsedStringAdapter; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; + +/** + * This is the abstract base class for a node of the {@link AccessControlSchema} that represents a tree of + * {@link AccessControlGroup}s and {@link AccessControlPermission}s. If a {@link java.security.Principal} "has" a + * {@link AccessControl} he also "has" all {@link AccessControl}s with according permissions in the spanned sub-tree. + * + */ +@XmlAccessorType(XmlAccessType.FIELD) +public abstract class AccessControl implements Serializable { + + /** UID for serialization. */ + private static final long serialVersionUID = 1L; + + /** @see #getId() */ + @XmlID + @XmlAttribute(name = "id", required = true) + @XmlJavaTypeAdapter(CollapsedStringAdapter.class) + @XmlSchemaType(name = "NCName") + private String id; + + /** + * The constructor. + */ + public AccessControl() { + + super(); + } + + /** + * The constructor. + * + * @param id the {@link #getId() ID}. + */ + public AccessControl(String id) { + + super(); + this.id = id; + } + + /** + * @return the unique identifier of this {@link AccessControl}. Has to be unique for all {@link AccessControl} in a + * {@link AccessControlSchema}. + */ + public String getId() { + + return this.id; + } + + /** + * @param id the new {@link #getId() id}. + */ + public void setId(String id) { + + this.id = id; + } + + @Override + public int hashCode() { + + return Objects.hash(this.id); + } + + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + AccessControl other = (AccessControl) obj; + if (!Objects.equals(this.id, other.id)) { + return false; + } + return true; + } + + @Override + public String toString() { + + return this.id; + } + +} diff --git a/modules/security/src/main/java/com/devonfw/module/security/common/api/accesscontrol/AccessControlGroup.java b/modules/security/src/main/java/com/devonfw/module/security/common/api/accesscontrol/AccessControlGroup.java new file mode 100644 index 00000000..94e41159 --- /dev/null +++ b/modules/security/src/main/java/com/devonfw/module/security/common/api/accesscontrol/AccessControlGroup.java @@ -0,0 +1,145 @@ +package com.devonfw.module.security.common.api.accesscontrol; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlIDREF; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlSchemaType; + +/** + * A {@link AccessControlGroup} represents a collection of {@link AccessControlPermission permissions}. A security + * administrator assigns a {@link java.security.Principal user} to a {@link AccessControlGroup group} to grant him the + * {@link AccessControlPermission permissions} of that {@link AccessControlGroup group}.
+ * Please note that a role is a special form of a {@link AccessControlGroup group} that also represents a + * strategic function. Therefore not every {@link AccessControlGroup group} is a role. Often a user can only have one + * role or can only act under one role at a time. Unfortunately these terms are often mixed up what is causing + * confusion. + * + */ +@XmlRootElement(name = "group") +public class AccessControlGroup extends AccessControl { // implements java.security.acl.Group { + + /** UID for serialization. */ + private static final long serialVersionUID = 1L; + + /** @see #getInherits() */ + @XmlIDREF + @XmlElementWrapper(name = "inherits") + @XmlElement(name = "group-ref") + private List inherits; + + /** @see #getPermissions() */ + @XmlElementWrapper(name = "permissions") + @XmlElement(name = "permission") + private List permissions; + + /** @see #getType() */ + @XmlAttribute(name = "type", required = false) + @XmlSchemaType(name = "string") + private String type; + + /** + * The constructor. + */ + public AccessControlGroup() { + + super(); + } + + /** + * The constructor. + * + * @param id the {@link #getId() ID}. + */ + public AccessControlGroup(String id) { + + super(id); + } + + /** + * @return the type of this group. E.g. "role", "department", "use-case-group", etc. You can use this for your own + * purpose. + */ + public String getType() { + + if (this.type == null) { + return ""; + } + return this.type; + } + + /** + * @param type the type to set + */ + public void setType(String type) { + + this.type = type; + } + + /** + * @return inherits + */ + public List getInherits() { + + if (this.inherits == null) { + this.inherits = new ArrayList<>(); + } + return this.inherits; + } + + /** + * @param inherits the inherits to set + */ + public void setInherits(List inherits) { + + this.inherits = inherits; + } + + /** + * @return the {@link List} of {@link AccessControlPermission}s. + */ + public List getPermissions() { + + if (this.permissions == null) { + this.permissions = new ArrayList<>(); + } + return this.permissions; + } + + /** + * @param permissions the new {@link #getPermissions() permissions}. + */ + public void setPermissions(List permissions) { + + this.permissions = permissions; + } + + @Override + public int hashCode() { + + return Objects.hash(super.hashCode(), this.type); + } + + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + AccessControlGroup other = (AccessControlGroup) obj; + if (!Objects.equals(this.type, other.type)) { + return false; + } + // other attributes may be mutable and id should already be unique... + return true; + } + +} diff --git a/modules/security/src/main/java/com/devonfw/module/security/common/api/accesscontrol/AccessControlPermission.java b/modules/security/src/main/java/com/devonfw/module/security/common/api/accesscontrol/AccessControlPermission.java new file mode 100644 index 00000000..da60a76f --- /dev/null +++ b/modules/security/src/main/java/com/devonfw/module/security/common/api/accesscontrol/AccessControlPermission.java @@ -0,0 +1,45 @@ +package com.devonfw.module.security.common.api.accesscontrol; + +import javax.xml.bind.annotation.XmlRootElement; + +/** + * A {@link AccessControlPermission} represents an atomic permission of the application. Each operation (use-case) + * should have its own {@link AccessControlPermission permission}. These operations are secured referencing the + * {@link #getId() ID} of the {@link AccessControlPermission permission}. We do this by annotating the operation method + * with {@link javax.annotation.security.RolesAllowed} (from JSR 250). Please do not get confused by the name + * {@link javax.annotation.security.RolesAllowed} as we are not assigning roles (see also {@link AccessControlGroup}) + * but {@link AccessControlPermission permissions} instead. We want to use Java standards (such as + * {@link javax.annotation.security.RolesAllowed}) where suitable but assigning the allowed roles to a method would end + * up in unmaintainable system configurations if your application reaches a certain complexity.
+ *
+ * If a user is logged in and wants to invoke the operation he needs to own the required permission. Therefore his + * {@link AccessControlGroup}s (resp. roles) have to contain the {@link AccessControlPermission permission} + * {@link AccessControlGroup#getPermissions() directly} or {@link AccessControlGroup#getInherits() indirectly}.
+ * In order to avoid naming clashes you should use the name of the application component as prefix of the permission. + * + */ +@XmlRootElement(name = "permission") +public class AccessControlPermission extends AccessControl { + + /** UID for serialization. */ + private static final long serialVersionUID = 1L; + + /** + * The constructor. + */ + public AccessControlPermission() { + + super(); + } + + /** + * The constructor. + * + * @param id the {@link #getId() ID}. + */ + public AccessControlPermission(String id) { + + super(id); + } + +} diff --git a/modules/security/src/main/java/com/devonfw/module/security/common/api/accesscontrol/AccessControlProvider.java b/modules/security/src/main/java/com/devonfw/module/security/common/api/accesscontrol/AccessControlProvider.java new file mode 100644 index 00000000..bfaac829 --- /dev/null +++ b/modules/security/src/main/java/com/devonfw/module/security/common/api/accesscontrol/AccessControlProvider.java @@ -0,0 +1,54 @@ +package com.devonfw.module.security.common.api.accesscontrol; + +import java.util.Set; + +/** + * This is the interface for a provider of {@link AccessControl}s. It allows to + * {@link #collectAccessControlIds(String, Set) collect} all {@link AccessControl}s for an ID of a {@link AccessControl} + * (typically a {@link AccessControlGroup} or role). This is used to expand the groups provided by the access-manager + * (authentication and identity-management) to the full set of {@link AccessControlPermission permissions} of the + * {@link java.security.Principal user}.
+ * The actual authorization can then check individual permissions of the user by simply checking for + * {@link Set#contains(Object) contains} in the collected {@link Set}, what is very fast as security checks happen + * frequently. + * + * @see PrincipalAccessControlProvider + * + */ +public interface AccessControlProvider { + + /** + * @param id is the {@link AccessControl#getId() ID} of the requested {@link AccessControl}. + * @return the requested {@link AccessControl} or {@code null} if not found. + */ + AccessControl getAccessControl(String id); + + /** + * This method collects the {@link AccessControl#getId() IDs} of all {@link AccessControlPermission}s (or more + * precisely of all {@link AccessControl}s) contained in the {@link AccessControl} {@link AccessControl#getId() + * identified} by the given groupId. + * + * @see #collectAccessControls(String, Set) + * + * @param id is the {@link AccessControl#getId() ID} of the {@link AccessControl} (typically an + * {@link AccessControlGroup}) to collect. + * @param permissions is the {@link Set} where to {@link Set#add(Object) add} the collected + * {@link AccessControl#getId() IDs}. This will include the given groupId. + * @return {@code true} if the given groupId has been found, {@code false} otherwise. + */ + boolean collectAccessControlIds(String id, Set permissions); + + /** + * This method collects the {@link AccessControl}s contained in the {@link AccessControl} + * {@link AccessControl#getId() identified} by the given groupId. + * + * @param id is the {@link AccessControl#getId() ID} of the {@link AccessControl} (typically an + * {@link AccessControlGroup}) to collect. + * @param permissions is the {@link Set} where to {@link Set#add(Object) add} the collected {@link AccessControl}s. + * This will include the {@link AccessControl} {@link AccessControl#getId() identified} by the given + * groupId. + * @return {@code true} if the given groupId has been found, {@code false} otherwise. + */ + boolean collectAccessControls(String id, Set permissions); + +} diff --git a/modules/security/src/main/java/com/devonfw/module/security/common/api/accesscontrol/AccessControlSchema.java b/modules/security/src/main/java/com/devonfw/module/security/common/api/accesscontrol/AccessControlSchema.java new file mode 100644 index 00000000..1c1f0c20 --- /dev/null +++ b/modules/security/src/main/java/com/devonfw/module/security/common/api/accesscontrol/AccessControlSchema.java @@ -0,0 +1,80 @@ +package com.devonfw.module.security.common.api.accesscontrol; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * This class represents the security configuration for the mapping of {@link AccessControlGroup}s to + * {@link AccessControlPermission}s. Everything is properly annotated for JAXB (de)serialization from/to XML. + * + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlRootElement(name = "access-control-schema") +public class AccessControlSchema { + + /** @see #getGroups() */ + @XmlElement(name = "group") + private List groups; + + /** + * The constructor. + */ + public AccessControlSchema() { + + super(); + } + + /** + * @return the {@link List} of {@link AccessControlGroup}s contained in this {@link AccessControlSchema}. + */ + public List getGroups() { + + if (this.groups == null) { + this.groups = new ArrayList<>(); + } + return this.groups; + } + + /** + * @param groups the new {@link #getGroups() groups}. + */ + public void setGroups(List groups) { + + this.groups = groups; + } + + @Override + public int hashCode() { + + final int prime = 31; + int result = 1; + result = prime * result + ((this.groups == null) ? 0 : this.groups.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + AccessControlSchema other = (AccessControlSchema) obj; + if (!Objects.equals(this.groups, other.groups)) { + return false; + } + return true; + } + +} diff --git a/modules/security/src/main/java/com/devonfw/module/security/common/api/accesscontrol/PrincipalAccessControlProvider.java b/modules/security/src/main/java/com/devonfw/module/security/common/api/accesscontrol/PrincipalAccessControlProvider.java new file mode 100644 index 00000000..fceca53e --- /dev/null +++ b/modules/security/src/main/java/com/devonfw/module/security/common/api/accesscontrol/PrincipalAccessControlProvider.java @@ -0,0 +1,21 @@ +package com.devonfw.module.security.common.api.accesscontrol; + +import java.security.Principal; +import java.util.Collection; + +/** + * This is the interface for a provide that allows to {@link #getAccessControlIds(Principal) get the permission groups} + * for a {@link Principal}. + * + * @param

is the generic type of the {@link Principal} representing the user or subject. + * + */ +public interface PrincipalAccessControlProvider

{ + + /** + * @param principal is the {@link Principal} (user). + * @return the {@link Collection} of {@link AccessControl#getId() IDs} with the groups of the given {@link Principal}. + */ + Collection getAccessControlIds(P principal); + +} diff --git a/modules/security/src/main/java/com/devonfw/module/security/common/api/exception/InvalidConfigurationException.java b/modules/security/src/main/java/com/devonfw/module/security/common/api/exception/InvalidConfigurationException.java new file mode 100644 index 00000000..570b42ad --- /dev/null +++ b/modules/security/src/main/java/com/devonfw/module/security/common/api/exception/InvalidConfigurationException.java @@ -0,0 +1,35 @@ +package com.devonfw.module.security.common.api.exception; + +/** + * Signals an exception during reading the security configuration + * + */ +public class InvalidConfigurationException extends RuntimeException { + + /** + * Default serial version UID + */ + private static final long serialVersionUID = 1L; + + /** + * Creates a new {@link InvalidConfigurationException} with the given message + * + * @param message error message + */ + public InvalidConfigurationException(String message) { + + super(message); + } + + /** + * Creates a new {@link InvalidConfigurationException} with the given message and the given cause + * + * @param message error message + * @param ex cause of the created exception + */ + public InvalidConfigurationException(String message, Throwable ex) { + + super(message, ex); + } + +} diff --git a/modules/security/src/main/java/com/devonfw/module/security/common/base/accesscontrol/AbstractAccessControlBasedAuthenticationProvider.java b/modules/security/src/main/java/com/devonfw/module/security/common/base/accesscontrol/AbstractAccessControlBasedAuthenticationProvider.java new file mode 100644 index 00000000..8ef8c6f0 --- /dev/null +++ b/modules/security/src/main/java/com/devonfw/module/security/common/base/accesscontrol/AbstractAccessControlBasedAuthenticationProvider.java @@ -0,0 +1,213 @@ +package com.devonfw.module.security.common.base.accesscontrol; + +import java.security.Principal; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import javax.inject.Inject; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import com.devonfw.module.security.common.api.accesscontrol.AccessControl; +import com.devonfw.module.security.common.api.accesscontrol.AccessControlProvider; +import com.devonfw.module.security.common.api.accesscontrol.PrincipalAccessControlProvider; + +/** + * This is an implementation of {@link AbstractUserDetailsAuthenticationProvider} based on + * {@link PrincipalAccessControlProvider} and {@link AccessControlProvider}. + * @deprecated As of bug-fix release 2.1.2 the authentication mechanism changes. It is now based upon custom + * implementations of {@link UserDetailsService} in combination with {@link WebSecurityConfigurerAdapter}. + * For further information have a look at the sample application.
+ *
+ * + * @param is the generic type of the {@link UserDetails} implementation used to bridge with spring-security. + * @param + *

+ * is the generic type of the {@link Principal} for internal user representation to bridge with + * {@link PrincipalAccessControlProvider}. + * + */ +@Deprecated +public abstract class AbstractAccessControlBasedAuthenticationProvider + extends AbstractUserDetailsAuthenticationProvider { + + /** The {@link Logger} instance. */ + private static final Logger LOG = LoggerFactory.getLogger(AbstractAccessControlBasedAuthenticationProvider.class); + + private PrincipalAccessControlProvider

principalAccessControlProvider; + + private AccessControlProvider accessControlProvider; + + /** + * The constructor. + */ + public AbstractAccessControlBasedAuthenticationProvider() { + + } + + /** + * @param principalAccessControlProvider the {@link PrincipalAccessControlProvider} to {@link Inject}. + */ + @Inject + public void setPrincipalAccessControlProvider(PrincipalAccessControlProvider

principalAccessControlProvider) { + + this.principalAccessControlProvider = principalAccessControlProvider; + } + + /** + * @param accessControlProvider the {@link AccessControlProvider} to {@link Inject}. + */ + @Inject + public void setAccessControlProvider(AccessControlProvider accessControlProvider) { + + this.accessControlProvider = accessControlProvider; + } + + /** + * Here the actual authentication has to be implemented.
+ *
+ * + */ + @Override + protected void additionalAuthenticationChecks(UserDetails userDetails, + UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { + + // default implementation authentications via servlet API (container managed) + ServletRequestAttributes currentRequestAttributes = + (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); + + HttpServletRequest request = currentRequestAttributes.getRequest(); + String login = authentication.getName(); + String password = null; + Object credentials = authentication.getCredentials(); + if (credentials != null) { + password = credentials.toString(); + } + try { + request.login(login, password); + } catch (ServletException e) { + LOG.warn("Authentication failed: {}", e.toString()); + throw new BadCredentialsException("Authentication failed.", e); + } + authentication.setDetails(userDetails); + } + + /** + * Returns the {@link GrantedAuthority}s of the provided user identified by the {@code username}. + * + * @param username the name of the user + * @return the associated {@link GrantedAuthority}s + * @throws AuthenticationException if not principal is retrievable for the given {@code username} + */ + protected Set getAuthorities(String username) throws AuthenticationException { + + P principal = retrievePrincipal(username); + if (principal == null) { + LOG.warn("Failed to retrieve user for login {}.", username); + throw new UsernameNotFoundException(username); + } + + // determine granted authorities for spring-security... + Set authorities = new HashSet<>(); + Collection accessControlIds = this.principalAccessControlProvider.getAccessControlIds(principal); + Set accessControlSet = new HashSet<>(); + for (String id : accessControlIds) { + boolean success = this.accessControlProvider.collectAccessControls(id, accessControlSet); + if (!success) { + LOG.warn("Undefined access control {}.", id); + // authorities.add(new SimpleGrantedAuthority(id)); + } + } + for (AccessControl accessControl : accessControlSet) { + authorities.add(new AccessControlGrantedAuthority(accessControl)); + } + return authorities; + } + + /** + * Creates an instance of {@link UserDetails} that represent the user with the given username. + * + * @param username is the login of the user to create. + * @param password the password of the user. + * @param principal is the internal {@link Principal} that has been provided by + * {@link #retrievePrincipal(String, UsernamePasswordAuthenticationToken)}. + * @param authorities are the {@link GrantedAuthority granted authorities} or in other words the permissions of the + * user. + * @return the new user object. + */ + protected abstract U createUser(String username, String password, P principal, Set authorities); + + /** + * DEPRECATED + * + * Retrieves the internal {@link Principal} object representing the user. This can be any object implementing + * {@link Principal} and can contain additional user details such as profile data. This object is used to + * {@link PrincipalAccessControlProvider#getAccessControlIds(Principal) retrieve} the (top-level) + * {@link AccessControl}s that have been granted to the user. + * + * @param username is the login of the user. + * @param authentication is the {@link UsernamePasswordAuthenticationToken}. + * @return the {@link Principal}. + */ + @Deprecated + protected abstract P retrievePrincipal(String username, UsernamePasswordAuthenticationToken authentication); + + /** + * Retrieves the internal {@link Principal} object representing the user. This can be any object implementing + * {@link Principal} and can contain additional user details such as profile data. This object is used to + * {@link PrincipalAccessControlProvider#getAccessControlIds(Principal) retrieve} the (top-level) + * {@link AccessControl}s that have been granted to the user. + * + * @param username is the login of the user. + * @return the {@link Principal}. + */ + protected abstract P retrievePrincipal(String username); + + @Override + protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) + throws AuthenticationException { + + P principal = retrievePrincipal(username, authentication); + if (principal == null) { + LOG.warn("Failed to retrieve user for login {}.", username); + throw new UsernameNotFoundException(username); + } + + // determine granted authorities for spring-security... + Set authorities = new HashSet<>(); + Collection accessControlIds = this.principalAccessControlProvider.getAccessControlIds(principal); + Set accessControlSet = new HashSet<>(); + for (String id : accessControlIds) { + boolean success = this.accessControlProvider.collectAccessControls(id, accessControlSet); + if (!success) { + LOG.warn("Undefined access control {}.", id); + // authorities.add(new SimpleGrantedAuthority(id)); + } + } + for (AccessControl accessControl : accessControlSet) { + authorities.add(new AccessControlGrantedAuthority(accessControl)); + } + + String password = null; + Object credentials = authentication.getCredentials(); + if (credentials != null) { + password = credentials.toString(); + } + return createUser(username, password, principal, authorities); + } +} diff --git a/modules/security/src/main/java/com/devonfw/module/security/common/base/accesscontrol/AbstractAccessControlProvider.java b/modules/security/src/main/java/com/devonfw/module/security/common/base/accesscontrol/AbstractAccessControlProvider.java new file mode 100644 index 00000000..8e7c55db --- /dev/null +++ b/modules/security/src/main/java/com/devonfw/module/security/common/base/accesscontrol/AbstractAccessControlProvider.java @@ -0,0 +1,227 @@ +package com.devonfw.module.security.common.base.accesscontrol; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.devonfw.module.security.common.api.accesscontrol.AccessControl; +import com.devonfw.module.security.common.api.accesscontrol.AccessControlGroup; +import com.devonfw.module.security.common.api.accesscontrol.AccessControlPermission; +import com.devonfw.module.security.common.api.accesscontrol.AccessControlProvider; +import com.devonfw.module.security.common.api.accesscontrol.AccessControlSchema; + +/** + * This is the abstract base implementation of {@link AccessControlProvider}.
+ * ATTENTION:
+ * You need to call {@link #initialize(AccessControlSchema)} from the derived implementation. + * + */ +public abstract class AbstractAccessControlProvider implements AccessControlProvider { + + /** Logger instance. */ + private static final Logger LOG = LoggerFactory.getLogger(AbstractAccessControlProvider.class); + + /** @see #getAccessControl(String) */ + private final Map id2nodeMap; + + /** + * The constructor. + */ + public AbstractAccessControlProvider() { + + super(); + this.id2nodeMap = new HashMap<>(); + } + + /** + * Performs the required initialization of this class. + * + * @param config is the {@link AccessControlSchema}. + */ + protected void initialize(AccessControlSchema config) { + + LOG.debug("Initializing."); + List groups = config.getGroups(); + if (groups.size() == 0) { + throw new IllegalStateException("AccessControlSchema is empty - please configure at least one group!"); + } + Set toplevelGroups = new HashSet<>(groups); + for (AccessControlGroup group : groups) { + collectAccessControls(group, toplevelGroups); + List groupList = new ArrayList<>(); + groupList.add(group); + checkForCyclicDependencies(group, groupList); + } + } + + /** + * Checks that the given {@link AccessControlGroup} has no cyclic {@link AccessControlGroup#getInherits() inheritance + * graph}. + * + * @param group is the {@link AccessControlGroup} to check. + * @param groupList the {@link List} of visited {@link AccessControlGroup}s used to detect cycles. + */ + protected void checkForCyclicDependencies(AccessControlGroup group, List groupList) { + + for (AccessControlGroup inheritedGroup : group.getInherits()) { + if (groupList.contains(inheritedGroup)) { + StringBuilder sb = new StringBuilder("A cyclic dependency of access control groups has been detected: \n"); + for (int i = groupList.size() - 1; i >= 0; i--) { + AccessControlGroup node = groupList.get(i); + sb.append(node); + if (i > 0) { + sb.append("-->"); + } + } + throw new IllegalStateException(sb.toString()); + } + groupList.add(inheritedGroup); + checkForCyclicDependencies(inheritedGroup, groupList); + AccessControlGroup removed = groupList.remove(groupList.size() - 1); + assert (removed == inheritedGroup); + } + } + + /** + * Called from {@link #initialize(AccessControlSchema)} to collect all {@link AccessControl}s recursively. + * + * @param group the {@link AccessControlGroup} to traverse. + * @param toplevelGroups is the {@link Set} of all {@link AccessControlGroup}s from + * {@link AccessControlSchema#getGroups()}. + */ + protected void collectAccessControls(AccessControlGroup group, Set toplevelGroups) { + + if (!toplevelGroups.contains(group)) { + throw new IllegalStateException("Invalid group not declared as top-level group in schema: " + group); + } + AccessControl old = this.id2nodeMap.put(group.getId(), group); + if (old != null) { + LOG.debug("Already visited access control group {}", group); + if (old != group) { + throw new IllegalStateException( + "Invalid security configuration: duplicate groups with id " + group.getId() + "!"); + } + // group has already been visited, stop recursion... + return; + } else { + LOG.debug("Registered access control group {}", group); + } + for (AccessControlPermission permission : group.getPermissions()) { + old = this.id2nodeMap.put(permission.getId(), permission); + if (old != null) { + // throw new IllegalStateException("Invalid security configuration: duplicate permission with id " + // + permission.getId() + "!"); + LOG.warn("Security configuration contains duplicate permission with id {}.", permission.getId()); + } else { + LOG.debug("Registered access control permission {}", permission); + } + } + for (AccessControlGroup inheritedGroup : group.getInherits()) { + collectAccessControls(inheritedGroup, toplevelGroups); + } + } + + @Override + public AccessControl getAccessControl(String nodeId) { + + return this.id2nodeMap.get(nodeId); + } + + /** + * Registers the given {@link AccessControl} and may be used for configuration of access controls during + * bootstrapping. This method should not be used after the application startup (bootstrapping) has completed. + * + * @param accessControl the {@link AccessControl} to register. + */ + protected void addAccessControl(AccessControl accessControl) { + + String id = accessControl.getId(); + AccessControl existing = this.id2nodeMap.get(id); + if (existing == null) { + this.id2nodeMap.put(id, accessControl); + LOG.debug("Registered access control {}", accessControl); + } else if (existing == accessControl) { + LOG.debug("Access control {} was already registered.", accessControl); + } else { + throw new IllegalStateException("Duplicate access control with ID '" + id + "'."); + } + } + + @Override + public boolean collectAccessControlIds(String groupId, Set permissions) { + + AccessControl node = getAccessControl(groupId); + if (node instanceof AccessControlGroup) { + collectPermissionIds((AccessControlGroup) node, permissions); + } else { + // node does not exist or is a flat AccessControlPermission + permissions.add(groupId); + } + return (node != null); + } + + /** + * Recursive implementation of {@link #collectAccessControlIds(String, Set)} for {@link AccessControlGroup}s. + * + * @param group is the {@link AccessControlGroup} to traverse. + * @param permissions is the {@link Set} used to collect. + */ + public void collectPermissionIds(AccessControlGroup group, Set permissions) { + + boolean added = permissions.add(group.getId()); + if (!added) { + // we have already visited this node, stop recursion... + return; + } + for (AccessControlPermission permission : group.getPermissions()) { + permissions.add(permission.getId()); + } + for (AccessControlGroup inheritedGroup : group.getInherits()) { + collectPermissionIds(inheritedGroup, permissions); + } + } + + @Override + public boolean collectAccessControls(String groupId, Set permissions) { + + AccessControl node = getAccessControl(groupId); + if (node == null) { + return false; + } + if (node instanceof AccessControlGroup) { + collectPermissionNodes((AccessControlGroup) node, permissions); + } else { + // node is a flat AccessControlPermission + permissions.add(node); + } + return true; + } + + /** + * Recursive implementation of {@link #collectAccessControls(String, Set)} for {@link AccessControlGroup}s. + * + * @param group is the {@link AccessControlGroup} to traverse. + * @param permissions is the {@link Set} used to collect. + */ + public void collectPermissionNodes(AccessControlGroup group, Set permissions) { + + boolean added = permissions.add(group); + if (!added) { + // we have already visited this node, stop recursion... + return; + } + for (AccessControlPermission permission : group.getPermissions()) { + permissions.add(permission); + } + for (AccessControlGroup inheritedGroup : group.getInherits()) { + collectPermissionNodes(inheritedGroup, permissions); + } + } + +} diff --git a/modules/security/src/main/java/com/devonfw/module/security/common/base/accesscontrol/AccessControlConfig.java b/modules/security/src/main/java/com/devonfw/module/security/common/base/accesscontrol/AccessControlConfig.java new file mode 100644 index 00000000..0e07c7cb --- /dev/null +++ b/modules/security/src/main/java/com/devonfw/module/security/common/base/accesscontrol/AccessControlConfig.java @@ -0,0 +1,90 @@ +package com.devonfw.module.security.common.base.accesscontrol; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.devonfw.module.security.common.api.accesscontrol.AccessControl; +import com.devonfw.module.security.common.api.accesscontrol.AccessControlGroup; +import com.devonfw.module.security.common.api.accesscontrol.AccessControlPermission; + +/** + * {@link AbstractAccessControlProvider} for static configuration of + * {@link com.devonfw.module.security.common.api.accesscontrol.AccessControlSchema}. Instead of maintaining it as XML file + * you can directly configure it as code and therefore define and reference constants in annotations such as + * {@link javax.annotation.security.RolesAllowed}. + * + * @since 3.0.0 + */ +public abstract class AccessControlConfig extends AbstractAccessControlProvider { + + /** + * Creates a new {@link AccessControlPermission} for static configuration of access controls. + * + * @param id {@link AccessControlPermission#getId() ID} of {@link AccessControlPermission} to get or create. + * @return the existing {@link AccessControlPermission} for the given {@link AccessControlPermission#getId() ID} or a + * newly created and registered {@link AccessControlPermission}. + */ + protected AccessControlPermission permission(String id) { + + AccessControl accessControl = getAccessControl(id); + if (accessControl instanceof AccessControlPermission) { + return (AccessControlPermission) accessControl; + } else if (accessControl != null) { + throw new IllegalStateException("Duplicate access control for ID '" + id + "'."); + } + AccessControlPermission permission = new AccessControlPermission(id); + addAccessControl(permission); + return permission; + } + + /** + * Creates a new {@link AccessControlGroup} for static configuration of access controls. + * + * @param groupId {@link AccessControlGroup#getId() ID} of {@link AccessControlGroup} to create. + * @param permissionIds {@link AccessControlPermission#getId() ID}s of the {@link #permission(String) permissions} to + * {@link AccessControlGroup#getPermissions() use}. + * @return the newly created and registered {@link AccessControlGroup}. + */ + protected AccessControlGroup group(String groupId, String... permissionIds) { + + return group(groupId, Collections. emptyList(), permissionIds); + } + + /** + * Creates a new {@link AccessControlGroup} for static configuration of access controls. + * + * @param groupId {@link AccessControlGroup#getId() ID} of {@link AccessControlGroup} to create. + * @param inherit single {@link AccessControlGroup} to {@link AccessControlGroup#getInherits() inherit}. + * @param permissionIds {@link AccessControlPermission#getId() ID}s of the {@link #permission(String) permissions} to + * {@link AccessControlGroup#getPermissions() use}. + * @return the newly created and registered {@link AccessControlGroup}. + */ + protected AccessControlGroup group(String groupId, AccessControlGroup inherit, String... permissionIds) { + + return group(groupId, Collections.singletonList(inherit), permissionIds); + } + + /** + * Creates a new {@link AccessControlGroup} for static configuration of access controls. + * + * @param groupId {@link AccessControlGroup#getId() ID} of {@link AccessControlGroup} to create. + * @param inherits {@link List} of {@link AccessControlGroup} to {@link AccessControlGroup#getInherits() inherit}. + * @param permissionIds {@link AccessControlPermission#getId() ID}s of the {@link #permission(String) permissions} to + * {@link AccessControlGroup#getPermissions() use}. + * @return the newly created and registered {@link AccessControlGroup}. + */ + protected AccessControlGroup group(String groupId, List inherits, String... permissionIds) { + + AccessControlGroup group = new AccessControlGroup(groupId); + group.setInherits(inherits); + List permissions = new ArrayList<>(permissionIds.length); + for (String permissionId : permissionIds) { + permissions.add(permission(permissionId)); + } + group.setPermissions(permissions); + addAccessControl(group); + return group; + } + +} diff --git a/modules/security/src/main/java/com/devonfw/module/security/common/base/accesscontrol/AccessControlGrantedAuthority.java b/modules/security/src/main/java/com/devonfw/module/security/common/base/accesscontrol/AccessControlGrantedAuthority.java new file mode 100644 index 00000000..32d52d87 --- /dev/null +++ b/modules/security/src/main/java/com/devonfw/module/security/common/base/accesscontrol/AccessControlGrantedAuthority.java @@ -0,0 +1,52 @@ +package com.devonfw.module.security.common.base.accesscontrol; + +import java.util.Objects; + +import org.springframework.security.core.GrantedAuthority; + +import com.devonfw.module.security.common.api.accesscontrol.AccessControl; + +/** + * Implementation of {@link GrantedAuthority} for a {@link AccessControl}. + * + */ +public class AccessControlGrantedAuthority implements GrantedAuthority { + + /** UID for serialization. */ + private static final long serialVersionUID = 1L; + + private final AccessControl accessControl; + + /** + * The constructor. + * + * @param accessControl the {@link #getAccessControl() access control}. + */ + public AccessControlGrantedAuthority(AccessControl accessControl) { + + super(); + Objects.requireNonNull(accessControl, AccessControl.class.getSimpleName()); + this.accessControl = accessControl; + } + + /** + * @return the contained {@link AccessControl}. + */ + public AccessControl getAccessControl() { + + return this.accessControl; + } + + @Override + public String getAuthority() { + + return this.accessControl.getId(); + } + + @Override + public String toString() { + + return getAuthority(); + } + +} diff --git a/modules/security/src/main/java/com/devonfw/module/security/common/base/accesscontrol/AccessControlSchemaMapper.java b/modules/security/src/main/java/com/devonfw/module/security/common/base/accesscontrol/AccessControlSchemaMapper.java new file mode 100644 index 00000000..87d0718d --- /dev/null +++ b/modules/security/src/main/java/com/devonfw/module/security/common/base/accesscontrol/AccessControlSchemaMapper.java @@ -0,0 +1,33 @@ +package com.devonfw.module.security.common.base.accesscontrol; + +import java.io.InputStream; +import java.io.OutputStream; + +import com.devonfw.module.security.common.api.accesscontrol.AccessControlSchema; + +/** + * This is the interface to {@link #read(InputStream)} and {@link #write(AccessControlSchema, OutputStream)} the + * {@link AccessControlSchema}. + * + */ +public interface AccessControlSchemaMapper { + + /** + * Reads the {@link AccessControlSchema} from the given {@link InputStream}. + * + * @param in is the {@link InputStream} with {@link AccessControlSchema} to read. Has to be + * {@link InputStream#close() closed} by the caller of this method who created the stream. + * @return the {@link AccessControlSchema} represented by the given input. + */ + AccessControlSchema read(InputStream in); + + /** + * Writes the given {@link AccessControlSchema} to the given {@link OutputStream}. + * + * @param conf is the {@link AccessControlSchema} to write. + * @param out is the {@link OutputStream} where to write the {@link AccessControlSchema} to. Has to be + * {@link OutputStream#close() closed} by the caller of this method who created the stream. + */ + void write(AccessControlSchema conf, OutputStream out); + +} diff --git a/modules/security/src/main/java/com/devonfw/module/security/common/base/accesscontrol/AccessControlSchemaProvider.java b/modules/security/src/main/java/com/devonfw/module/security/common/base/accesscontrol/AccessControlSchemaProvider.java new file mode 100644 index 00000000..f701f7e8 --- /dev/null +++ b/modules/security/src/main/java/com/devonfw/module/security/common/base/accesscontrol/AccessControlSchemaProvider.java @@ -0,0 +1,18 @@ +package com.devonfw.module.security.common.base.accesscontrol; + +import com.devonfw.module.security.common.api.accesscontrol.AccessControlSchema; + +/** + * This is the interface to {@link #loadSchema() load} the {@link AccessControlSchema} from an arbitrary source. The + * default implementation will load it from an XML file. You could create your own implementation to read from database + * or wherever if default is not suitable. + * + */ +public interface AccessControlSchemaProvider { + + /** + * @return the loaded {@link AccessControlSchema}. May not be {@code null}. + */ + AccessControlSchema loadSchema(); + +} diff --git a/modules/security/src/main/java/com/devonfw/module/security/common/base/accesscontrol/PrincipalGroupProviderGroupImpl.java b/modules/security/src/main/java/com/devonfw/module/security/common/base/accesscontrol/PrincipalGroupProviderGroupImpl.java new file mode 100644 index 00000000..12a857bd --- /dev/null +++ b/modules/security/src/main/java/com/devonfw/module/security/common/base/accesscontrol/PrincipalGroupProviderGroupImpl.java @@ -0,0 +1,54 @@ +package com.devonfw.module.security.common.base.accesscontrol; + +import java.security.Principal; +import java.security.acl.Group; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; + +import com.devonfw.module.security.common.api.accesscontrol.PrincipalAccessControlProvider; + +/** + * This is an implementation of {@link PrincipalAccessControlProvider} based on {@link Group}. Due to the confusing API of + * {@link Group} that mixes a {@link Principal} with permissions and permission groups it is not commonly used even + * though it is available in the Java standard edition. + * + */ +public class PrincipalGroupProviderGroupImpl implements PrincipalAccessControlProvider { + + /** + * The constructor. + */ + public PrincipalGroupProviderGroupImpl() { + + super(); + } + + @Override + public Collection getAccessControlIds(Group principal) { + + Set groupSet = new HashSet<>(); + collectGroups(principal, groupSet); + return groupSet; + } + + /** + * Called from {@link #getAccessControlIds(Group)} to recursively collect the groups. + * + * @param group is the {@link Group} to traverse. + * @param groupSet is the {@link Set} where to add the principal names. + */ + protected void collectGroups(Group group, Set groupSet) { + + Enumeration members = group.members(); + while (members.hasMoreElements()) { + Principal member = members.nextElement(); + String name = member.getName(); + boolean added = groupSet.add(name); + if (added && (member instanceof Group)) { + collectGroups((Group) member, groupSet); + } + } + } +} diff --git a/modules/security/src/main/java/com/devonfw/module/security/common/base/accesscontrol/SimpleAccessControlBasedAuthenticationProvider.java b/modules/security/src/main/java/com/devonfw/module/security/common/base/accesscontrol/SimpleAccessControlBasedAuthenticationProvider.java new file mode 100644 index 00000000..d6aa16a0 --- /dev/null +++ b/modules/security/src/main/java/com/devonfw/module/security/common/base/accesscontrol/SimpleAccessControlBasedAuthenticationProvider.java @@ -0,0 +1,60 @@ +package com.devonfw.module.security.common.base.accesscontrol; + +import java.security.Principal; +import java.util.Set; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; + +import com.devonfw.module.security.common.api.accesscontrol.AccessControlProvider; +import com.devonfw.module.security.common.api.accesscontrol.PrincipalAccessControlProvider; + +/** + * @deprecated As of bug-fix release 2.1.2 the authentication mechanism changes. It is now based upon custom + * implementations of {@link UserDetailsService} in combination with {@link WebSecurityConfigurerAdapter}. + * For further information have a look at the sample application.
+ * This is an implementation of {@link AbstractUserDetailsAuthenticationProvider} based on + * {@link PrincipalAccessControlProvider} and {@link AccessControlProvider}.
+ *
+ * This is a simple implementation of {@link AbstractAccessControlBasedAuthenticationProvider}. + * + */ +@Deprecated +public class SimpleAccessControlBasedAuthenticationProvider + extends AbstractAccessControlBasedAuthenticationProvider { + + /** + * The constructor. + */ + public SimpleAccessControlBasedAuthenticationProvider() { + + super(); + } + + @Override + protected User createUser(String username, String password, Principal principal, Set authorities) { + + User user = new User(username, password, authorities); + return user; + } + + @Override + protected Principal retrievePrincipal(String username, UsernamePasswordAuthenticationToken authentication) { + + return authentication; + } + + /* + * Leave empty on purpose. Not used in this version. + */ + @Override + protected Principal retrievePrincipal(String username) { + + return null; + } + +} diff --git a/modules/security/src/main/java/com/devonfw/module/security/common/impl/accesscontrol/AccessControlProviderImpl.java b/modules/security/src/main/java/com/devonfw/module/security/common/impl/accesscontrol/AccessControlProviderImpl.java new file mode 100644 index 00000000..de0e0fd5 --- /dev/null +++ b/modules/security/src/main/java/com/devonfw/module/security/common/impl/accesscontrol/AccessControlProviderImpl.java @@ -0,0 +1,54 @@ +package com.devonfw.module.security.common.impl.accesscontrol; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; + +import com.devonfw.module.security.common.base.accesscontrol.AbstractAccessControlProvider; +import com.devonfw.module.security.common.base.accesscontrol.AccessControlSchemaProvider; + +/** + * This is the default implementation of {@link com.devonfw.module.security.common.api.accesscontrol.AccessControlProvider}. + * + */ +public class AccessControlProviderImpl extends AbstractAccessControlProvider { + + private AccessControlSchemaProvider accessControlSchemaProvider; + + /** + * The constructor. + */ + public AccessControlProviderImpl() { + + super(); + } + + /** + * Initializes this class. + */ + @PostConstruct + public void initialize() { + + if (this.accessControlSchemaProvider == null) { + this.accessControlSchemaProvider = new AccessControlSchemaProviderImpl(); + } + initialize(this.accessControlSchemaProvider.loadSchema()); + } + + /** + * @return accessControlSchemaProvider + */ + public AccessControlSchemaProvider getAccessControlSchemaProvider() { + + return this.accessControlSchemaProvider; + } + + /** + * @param accessControlSchemaProvider the {@link AccessControlSchemaProvider} to {@link Inject}. + */ + @Inject + public void setAccessControlSchemaProvider(AccessControlSchemaProvider accessControlSchemaProvider) { + + this.accessControlSchemaProvider = accessControlSchemaProvider; + } + +} diff --git a/modules/security/src/main/java/com/devonfw/module/security/common/impl/accesscontrol/AccessControlSchemaProviderImpl.java b/modules/security/src/main/java/com/devonfw/module/security/common/impl/accesscontrol/AccessControlSchemaProviderImpl.java new file mode 100644 index 00000000..0a28fe88 --- /dev/null +++ b/modules/security/src/main/java/com/devonfw/module/security/common/impl/accesscontrol/AccessControlSchemaProviderImpl.java @@ -0,0 +1,106 @@ +package com.devonfw.module.security.common.impl.accesscontrol; + +import java.io.InputStream; + +import javax.annotation.PostConstruct; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +import com.devonfw.module.security.common.api.accesscontrol.AccessControlSchema; +import com.devonfw.module.security.common.base.accesscontrol.AccessControlSchemaMapper; +import com.devonfw.module.security.common.base.accesscontrol.AccessControlSchemaProvider; + +/** + * This is the default implementation of {@link AccessControlSchemaProvider}. + * + */ +public class AccessControlSchemaProviderImpl implements AccessControlSchemaProvider { + + /** Logger instance. */ + private static final Logger LOG = LoggerFactory.getLogger(AccessControlSchemaProviderImpl.class); + + private Resource accessControlSchema; + + private AccessControlSchemaMapper accessControlSchemaMapper; + + private boolean initialized; + + /** + * The constructor. + */ + public AccessControlSchemaProviderImpl() { + + super(); + this.initialized = false; + } + + /** + * Initializes this class. + */ + @PostConstruct + public void initialize() { + + if (this.initialized) { + return; + } + LOG.debug("Initializing."); + if (this.accessControlSchemaMapper == null) { + this.accessControlSchemaMapper = new AccessControlSchemaXmlMapper(); + } + if (this.accessControlSchema == null) { + + this.accessControlSchema = new ClassPathResource("config/app/security/access-control-schema.xml"); + } + this.initialized = true; + } + + @Override + public AccessControlSchema loadSchema() { + + initialize(); + LOG.debug("Reading access control schema from {}", this.accessControlSchema); + try (InputStream inputStream = this.accessControlSchema.getInputStream()) { + AccessControlSchema schema = this.accessControlSchemaMapper.read(inputStream); + LOG.debug("Reading access control schema completed successfully."); + return schema; + } catch (Exception e) { + throw new IllegalStateException("Failed to load access control schema from " + this.accessControlSchema, e); + } + } + + /** + * @return the {@link AccessControlSchemaMapper}. + */ + public AccessControlSchemaMapper getAccessControlSchemaMapper() { + + return this.accessControlSchemaMapper; + } + + /** + * @param accessControlSchemaMapper the {@link AccessControlSchemaMapper} to use. + */ + public void setAccessControlSchemaMapper(AccessControlSchemaMapper accessControlSchemaMapper) { + + this.accessControlSchemaMapper = accessControlSchemaMapper; + } + + /** + * @return accessControlSchema + */ + public Resource getAccessControlSchema() { + + return this.accessControlSchema; + } + + /** + * @param accessControlSchema the {@link Resource} pointing to the XML configuration of the access control schema. + */ + public void setAccessControlSchema(Resource accessControlSchema) { + + this.accessControlSchema = accessControlSchema; + } + +} diff --git a/modules/security/src/main/java/com/devonfw/module/security/common/impl/accesscontrol/AccessControlSchemaXmlMapper.java b/modules/security/src/main/java/com/devonfw/module/security/common/impl/accesscontrol/AccessControlSchemaXmlMapper.java new file mode 100644 index 00000000..ef071449 --- /dev/null +++ b/modules/security/src/main/java/com/devonfw/module/security/common/impl/accesscontrol/AccessControlSchemaXmlMapper.java @@ -0,0 +1,152 @@ +package com.devonfw.module.security.common.impl.accesscontrol; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.SchemaOutputResolver; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.ValidationEvent; +import javax.xml.bind.ValidationEventHandler; +import javax.xml.transform.Result; +import javax.xml.transform.stream.StreamResult; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.devonfw.module.security.common.api.accesscontrol.AccessControlSchema; +import com.devonfw.module.security.common.base.accesscontrol.AccessControlSchemaMapper; + +/** + * This class is a simple wrapper for {@link #read(InputStream) reading} and + * {@link #write(AccessControlSchema, OutputStream) writing} the {@link AccessControlSchema} from/to XML. + * + */ +public class AccessControlSchemaXmlMapper implements AccessControlSchemaMapper { + + /** Logger instance. */ + private static final Logger LOG = LoggerFactory.getLogger(AccessControlSchemaXmlMapper.class); + + private JAXBContext jaxbContext; + + /** + * The constructor. + */ + public AccessControlSchemaXmlMapper() { + + super(); + try { + this.jaxbContext = JAXBContext.newInstance(AccessControlSchema.class); + } catch (JAXBException e) { + throw new IllegalStateException(e); + } + } + + @Override + public void write(AccessControlSchema conf, OutputStream out) { + + try { + Marshaller marshaller = this.jaxbContext.createMarshaller(); + marshaller.marshal(conf, out); + } catch (JAXBException e) { + throw new IllegalStateException("Marshalling XML failed!", e); + } + } + + @Override + public AccessControlSchema read(InputStream in) { + + try { + Unmarshaller unmarshaller = this.jaxbContext.createUnmarshaller(); + ValidationEventHandler handler = new ValidationEventHandlerImpl(); + unmarshaller.setEventHandler(handler); + return (AccessControlSchema) unmarshaller.unmarshal(in); + } catch (JAXBException e) { + throw new IllegalStateException("Unmarshalling XML failed!", e); + } + } + + /** + * Generates the XSD (XML Schema Definition) to the given {@link File}. + * + * @param outFile is the {@link File} to write to. + */ + public void writeXsd(File outFile) { + + File folder = outFile.getParentFile(); + if (!folder.isDirectory()) { + boolean success = folder.mkdirs(); + if (!success) { + throw new IllegalStateException("Failed to create folder " + folder); + } + } + try (FileOutputStream fos = new FileOutputStream(outFile)) { + writeXsd(fos); + } catch (Exception e) { + throw new IllegalStateException("Failed to generate and write the XSD schema to " + outFile + "!", e); + } + } + + /** + * Generates the XSD (XML Schema Definition) to the given {@link OutputStream}. + * + * @param out is the {@link OutputStream} to write to. + */ + public void writeXsd(final OutputStream out) { + + SchemaOutputResolver sor = new SchemaOutputResolver() { + + @Override + public Result createOutput(String namespaceUri, String suggestedFileName) throws IOException { + + StreamResult streamResult = new StreamResult(out); + streamResult.setSystemId(suggestedFileName); + return streamResult; + } + + }; + try { + this.jaxbContext.generateSchema(sor); + } catch (IOException e) { + throw new IllegalStateException("Failed to generate and write the XSD schema!", e); + } + } + + /** + * Custom implementation of {@link ValidationEventHandler}. + */ + protected static class ValidationEventHandlerImpl implements ValidationEventHandler { + + /** + * The constructor. + */ + public ValidationEventHandlerImpl() { + + super(); + } + + @Override + public boolean handleEvent(ValidationEvent event) { + + if (event != null) { + switch (event.getSeverity()) { + case ValidationEvent.ERROR: + case ValidationEvent.FATAL_ERROR: + throw new IllegalArgumentException(event.toString()); + case ValidationEvent.WARNING: + LOG.warn(event.toString()); + break; + default: + LOG.debug(event.toString()); + } + } + return true; + } + } + +} diff --git a/modules/security/src/main/java/com/devonfw/module/security/common/impl/accesscontrol/AccessControlSchemaXsdWriter.java b/modules/security/src/main/java/com/devonfw/module/security/common/impl/accesscontrol/AccessControlSchemaXsdWriter.java new file mode 100644 index 00000000..1c4737e0 --- /dev/null +++ b/modules/security/src/main/java/com/devonfw/module/security/common/impl/accesscontrol/AccessControlSchemaXsdWriter.java @@ -0,0 +1,31 @@ +package com.devonfw.module.security.common.impl.accesscontrol; + +import java.io.File; + +/** + * This is a simple programm to generate (create or update) the XSD for the + * {@link com.devonfw.module.security.common.api.accesscontrol.AccessControlSchema}. + * + */ +public class AccessControlSchemaXsdWriter extends AccessControlSchemaXmlMapper { + + /** + * The constructor. + */ + public AccessControlSchemaXsdWriter() { + + super(); + } + + /** + * The main method to launch this program. + * + * @param args the command-line arguments (will be ignored). + */ + public static void main(String[] args) { + + AccessControlSchemaXmlMapper mapper = new AccessControlSchemaXmlMapper(); + mapper.writeXsd(new File("src/main/resources/com/devonfw/module/security/access-control-schema.xsd")); + } + +} diff --git a/modules/security/src/main/java/com/devonfw/module/security/common/impl/rest/AuthenticationSuccessHandlerSendingOkHttpStatusCode.java b/modules/security/src/main/java/com/devonfw/module/security/common/impl/rest/AuthenticationSuccessHandlerSendingOkHttpStatusCode.java new file mode 100644 index 00000000..c10e3bea --- /dev/null +++ b/modules/security/src/main/java/com/devonfw/module/security/common/impl/rest/AuthenticationSuccessHandlerSendingOkHttpStatusCode.java @@ -0,0 +1,39 @@ +package com.devonfw.module.security.common.impl.rest; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.springframework.security.core.Authentication; +import org.springframework.security.web.WebAttributes; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +/** + * Sends the OK status code upon successful authentication. + * + * @see JsonUsernamePasswordAuthenticationFilter + */ +public class AuthenticationSuccessHandlerSendingOkHttpStatusCode implements AuthenticationSuccessHandler { + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + + if (response.isCommitted()) { + return; + } + clearAuthenticationAttributes(request); + response.setStatus(HttpServletResponse.SC_OK); + } + + private void clearAuthenticationAttributes(HttpServletRequest request) { + + HttpSession session = request.getSession(false); + if (session == null) { + return; + } + session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); + } +} diff --git a/modules/security/src/main/java/com/devonfw/module/security/common/impl/rest/JsonUsernamePasswordAuthenticationFilter.java b/modules/security/src/main/java/com/devonfw/module/security/common/impl/rest/JsonUsernamePasswordAuthenticationFilter.java new file mode 100644 index 00000000..0c94880f --- /dev/null +++ b/modules/security/src/main/java/com/devonfw/module/security/common/impl/rest/JsonUsernamePasswordAuthenticationFilter.java @@ -0,0 +1,217 @@ +package com.devonfw.module.security.common.impl.rest; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.util.matcher.RequestMatcher; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + *

+ * Processes authentication where credentials are sent as a JSON object. + *

+ *

+ * The JSON object must contain two properties: a username and a password. The default properties' names to use are + * contained in the static fields {@link UsernamePasswordAuthenticationFilter#SPRING_SECURITY_FORM_USERNAME_KEY} and + * {@link UsernamePasswordAuthenticationFilter#SPRING_SECURITY_FORM_PASSWORD_KEY}. The JSON object properties' names can + * also be changed by setting the {@code usernameParameter} and {@code passwordParameter} properties. Assuming the + * default properties' names were not changed, if the credentials user/pass are to be sent, + * the following JSON object is expected: + * + *

+ * 
+ *     {
+ *        "j_username": "user",
+ *        "j_password": "pass",
+ *    }
+ * 
+ * 
+ *

+ *

+ * The URL this filter responds to is passed as a constructor parameter. + *

+ *

+ * This authentication filter is intended for One Page Applications which handle a login page/dialog/pop-up on their + * own. This filter combined with: + *

    + *
  • {@link AuthenticationSuccessHandlerSendingOkHttpStatusCode}
  • + *
  • {@link org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler} created using the + * default constructor (thus leaving the {@code defaultFailureUrl} unset)
  • + *
  • {@link LogoutSuccessHandlerReturningOkHttpStatusCode}
  • + *
+ * makes the login/logout API fully RESTful. + *

+ * + */ +public class JsonUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { + private String usernameParameter = UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY; + + private String passwordParameter = UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY; + + private boolean postOnly = true; + + // REVIEW may-bee (hohwille) We have a centralized and custom-configured object mapper as spring bean. IMHO we should + // inject that instance here. + private ObjectMapper objectMapper = new ObjectMapper(); + + /** + * The constructor. + * + * @param requiresAuthenticationRequestMatcher the {@link RequestMatcher} used to determine if authentication is + * required. Cannot be null. + */ + public JsonUsernamePasswordAuthenticationFilter(RequestMatcher requiresAuthenticationRequestMatcher) { + + super(requiresAuthenticationRequestMatcher); + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) + throws AuthenticationException, IOException, ServletException { + + if (this.postOnly && !request.getMethod().equals("POST")) { + throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); + } + + final UsernameAndPasswordParser usernameAndPasswordParser = new UsernameAndPasswordParser(request); + usernameAndPasswordParser.parse(); + UsernamePasswordAuthenticationToken authRequest = + new UsernamePasswordAuthenticationToken(usernameAndPasswordParser.getTrimmedUsername(), + usernameAndPasswordParser.getPassword()); + // authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); + return getAuthenticationManager().authenticate(authRequest); + } + + /** + * @return Value of usernameParameter + */ + public String getUsernameParameter() { + + return this.usernameParameter; + } + + /** + * @param usernameParameter new value for usernameParameter + */ + public void setUsernameParameter(String usernameParameter) { + + this.usernameParameter = usernameParameter; + } + + /** + * @return Value of passwordParameter + */ + public String getPasswordParameter() { + + return this.passwordParameter; + } + + /** + * @param passwordParameter new value for passwordParameter + */ + public void setPasswordParameter(String passwordParameter) { + + this.passwordParameter = passwordParameter; + } + + /** + * @return value of postOnly + */ + public boolean isPostOnly() { + + return this.postOnly; + } + + /** + * @param postOnly new value for postOnly + */ + public void setPostOnly(boolean postOnly) { + + this.postOnly = postOnly; + } + + private class UsernameAndPasswordParser { + private String username; + + private String password; + + private final HttpServletRequest request; + + private JsonNode credentialsNode; + + private UsernameAndPasswordParser(HttpServletRequest request) { + + this.request = request; + } + + public void parse() { + + parseJsonFromRequestBody(); + if (jsonParsedSuccessfully()) { + extractUsername(); + extractPassword(); + } + } + + private void extractPassword() { + + this.password = extractValueByName(JsonUsernamePasswordAuthenticationFilter.this.passwordParameter); + } + + private void extractUsername() { + + this.username = extractValueByName(JsonUsernamePasswordAuthenticationFilter.this.usernameParameter); + } + + private String extractValueByName(String name) { + + String value = null; + if (this.credentialsNode.has(name)) { + JsonNode node = this.credentialsNode.get(name); + if (node != null) { + value = node.asText(); + } + } + return value; + } + + private boolean jsonParsedSuccessfully() { + + return this.credentialsNode != null; + } + + private void parseJsonFromRequestBody() { + + try { + final ServletServerHttpRequest servletServerHttpRequest = new ServletServerHttpRequest(this.request); + this.credentialsNode = + JsonUsernamePasswordAuthenticationFilter.this.objectMapper.readTree(servletServerHttpRequest.getBody()); + } catch (IOException e) { + // ignoring + } + } + + private String getTrimmedUsername() { + + return this.username == null ? "" : this.username.trim(); + } + + private String getPassword() { + + return this.password == null ? "" : this.password; + } + + } +} diff --git a/modules/security/src/main/java/com/devonfw/module/security/common/impl/rest/LogoutSuccessHandlerReturningOkHttpStatusCode.java b/modules/security/src/main/java/com/devonfw/module/security/common/impl/rest/LogoutSuccessHandlerReturningOkHttpStatusCode.java new file mode 100644 index 00000000..15c4baf9 --- /dev/null +++ b/modules/security/src/main/java/com/devonfw/module/security/common/impl/rest/LogoutSuccessHandlerReturningOkHttpStatusCode.java @@ -0,0 +1,30 @@ +package com.devonfw.module.security.common.impl.rest; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; + +/** + * Sends the OK status code upon successful logout. + * + * @see JsonUsernamePasswordAuthenticationFilter + */ +public class LogoutSuccessHandlerReturningOkHttpStatusCode implements LogoutSuccessHandler { + /** + * Called after a successful logout by the {@link JsonUsernamePasswordAuthenticationFilter}. + */ + @Override + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) + throws IOException, ServletException { + + if (response.isCommitted()) { + return; + } + response.setStatus(HttpServletResponse.SC_OK); + } +} diff --git a/modules/security/src/main/java/com/devonfw/module/security/common/impl/web/RetainAnchorFilter.java b/modules/security/src/main/java/com/devonfw/module/security/common/impl/web/RetainAnchorFilter.java new file mode 100644 index 00000000..565daa44 --- /dev/null +++ b/modules/security/src/main/java/com/devonfw/module/security/common/impl/web/RetainAnchorFilter.java @@ -0,0 +1,188 @@ +package com.devonfw.module.security.common.impl.web; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +import org.springframework.web.filter.GenericFilterBean; + +/** + * Spring Security filter that preserves the URL anchor if the authentication process contains redirects (e.g. if the + * login is performed via CAS or form login). + * + * With standard redirects (default Spring Security behavior), Internet Explorer (6.0 and 8.0) discard the anchor part + * of the URL such that e.g. bookmarking does not work properly. Firefox re-appends the anchor part. + * + * This filter replaces redirects to URLs that match a certain pattern (storeUrlPattern) with a Javascript + * page that stores the URL anchor in a cookie, and replaces redirects to URLs that match another pattern ( + * restoreUrlPattern) with a Javascript page that restores the URL anchor from that cookie. The cookie name + * can be set via the attribute cookieName. + * + * @see Forum post of guidow08 + * @see Forum post of mpickell + */ +public class RetainAnchorFilter extends GenericFilterBean { + + private String storeUrlPattern; + + private String restoreUrlPattern; + + private String cookieName; + + /** + * Sets the url pattern for storing anchors. + * + * @param storeUrlPattern url regular expression + */ + public void setStoreUrlPattern(String storeUrlPattern) { + + this.storeUrlPattern = storeUrlPattern; + } + + /** + * Sets the url pattern for restoring anchors. + * + * @param restoreUrlPattern url regular expression + */ + public void setRestoreUrlPattern(String restoreUrlPattern) { + + this.restoreUrlPattern = restoreUrlPattern; + } + + /** + * Sets the cookie name in which the anchor data should be saved. + * + * @param cookieName name of the cookie + */ + public void setCookieName(String cookieName) { + + this.cookieName = cookieName; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, + ServletException { + + ServletResponse wrappedResponse = response; + if (response instanceof HttpServletResponse) { + wrappedResponse = new RedirectResponseWrapper((HttpServletResponse) response); + } + + chain.doFilter(request, wrappedResponse); + } + + /** + * HttpServletResponseWrapper that replaces the redirect by appropriate Javascript code. + */ + private class RedirectResponseWrapper extends HttpServletResponseWrapper { + + public RedirectResponseWrapper(HttpServletResponse response) { + + super(response); + } + + @Override + public void sendRedirect(String location) throws IOException { + + HttpServletResponse response = (HttpServletResponse) getResponse(); + String redirectPageHtml = ""; + if (location.matches(RetainAnchorFilter.this.storeUrlPattern)) { + redirectPageHtml = generateStoreAnchorRedirectPageHtml(location); + } else if (location.matches(RetainAnchorFilter.this.restoreUrlPattern)) { + redirectPageHtml = generateRestoreAnchorRedirectPageHtml(location); + } else { + super.sendRedirect(location); + return; + } + response.setContentType("text/html;charset=UTF-8"); + response.setContentLength(redirectPageHtml.length()); + response.getWriter().write(redirectPageHtml); + } + + private String generateStoreAnchorRedirectPageHtml(String location) { + + StringBuilder sb = new StringBuilder(); + + sb.append("Redirect Page\n"); + sb.append("\n\n"); + sb.append("

Redirect Page (Store Anchor)

\n"); + sb.append("Should redirect to " + location + "\n"); + sb.append("\n"); + + return sb.toString(); + } + + /** + * @see Forum + * post + */ + private String generateRestoreAnchorRedirectPageHtml(String location) { + + StringBuilder sb = new StringBuilder(); + + // open html + sb.append("Redirect Page"); + sb.append(""); + sb.append(""); + sb.append(""); + sb.append("

Redirect Page (Restore Anchor)

"); + sb.append("

Should redirect to " + location + "

"); + sb.append(""); + sb.append(""); + + return sb.toString(); + } + + } +} diff --git a/modules/security/src/main/resources/com/devonfw/module/security/access-control-schema.xsd b/modules/security/src/main/resources/com/devonfw/module/security/access-control-schema.xsd new file mode 100644 index 00000000..4b256afb --- /dev/null +++ b/modules/security/src/main/resources/com/devonfw/module/security/access-control-schema.xsd @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/security/src/test/java/com/devonfw/module/security/common/impl/accesscontrol/AccessControlConfigSimple.java b/modules/security/src/test/java/com/devonfw/module/security/common/impl/accesscontrol/AccessControlConfigSimple.java new file mode 100644 index 00000000..c7bd1a99 --- /dev/null +++ b/modules/security/src/test/java/com/devonfw/module/security/common/impl/accesscontrol/AccessControlConfigSimple.java @@ -0,0 +1,89 @@ +package com.devonfw.module.security.common.impl.accesscontrol; + +import javax.inject.Named; + +import com.devonfw.module.security.common.api.accesscontrol.AccessControlGroup; +import com.devonfw.module.security.common.base.accesscontrol.AccessControlConfig; + +/** + * Example of {@link AccessControlConfig} that used for testing. + */ +@Named +public class AccessControlConfigSimple extends AccessControlConfig { + + public static final String APP_ID = "MyApp"; + + private static final String PREFIX = APP_ID + "."; + + public static final String PERMISSION_FIND_OFFER = PREFIX + "FindOffer"; + + public static final String PERMISSION_SAVE_OFFER = PREFIX + "SaveOffer"; + + public static final String PERMISSION_DELETE_OFFER = PREFIX + "DeleteOffer"; + + public static final String PERMISSION_FIND_PRODUCT = PREFIX + "FindProduct"; + + public static final String PERMISSION_SAVE_PRODUCT = PREFIX + "SaveProduct"; + + public static final String PERMISSION_DELETE_PRODUCT = PREFIX + "DeleteProduct"; + + public static final String PERMISSION_FIND_TABLE = PREFIX + "FindTable"; + + public static final String PERMISSION_SAVE_TABLE = PREFIX + "SaveTable"; + + public static final String PERMISSION_DELETE_TABLE = PREFIX + "DeleteTable"; + + public static final String PERMISSION_FIND_STAFF_MEMBER = PREFIX + "FindStaffMember"; + + public static final String PERMISSION_SAVE_STAFF_MEMBER = PREFIX + "SaveStaffMember"; + + public static final String PERMISSION_DELETE_STAFF_MEMBER = PREFIX + "DeleteStaffMember"; + + public static final String PERMISSION_FIND_ORDER = PREFIX + "FindOrder"; + + public static final String PERMISSION_SAVE_ORDER = PREFIX + "SaveOrder"; + + public static final String PERMISSION_DELETE_ORDER = PREFIX + "DeleteOrder"; + + public static final String PERMISSION_FIND_ORDER_POSITION = PREFIX + "FindOrderPosition"; + + public static final String PERMISSION_SAVE_ORDER_POSITION = PREFIX + "SaveOrderPosition"; + + public static final String PERMISSION_DELETE_ORDER_POSITION = PREFIX + "DeleteOrderPosition"; + + public static final String PERMISSION_FIND_BILL = PREFIX + "FindBill"; + + public static final String PERMISSION_SAVE_BILL = PREFIX + "SaveBill"; + + public static final String PERMISSION_DELETE_BILL = PREFIX + "DeleteBill"; + + public static final String GROUP_READ_MASTER_DATA = PREFIX + "ReadMasterData"; + + public static final String GROUP_COOK = PREFIX + "Cook"; + + public static final String GROUP_BARKEEPER = PREFIX + "Barkeeper"; + + public static final String GROUP_WAITER = PREFIX + "Waiter"; + + public static final String GROUP_CHIEF = PREFIX + "Chief"; + + /** + * The constructor. + */ + public AccessControlConfigSimple() { + + super(); + AccessControlGroup readMasterData = group(GROUP_READ_MASTER_DATA, PERMISSION_FIND_OFFER, PERMISSION_FIND_PRODUCT, + PERMISSION_FIND_STAFF_MEMBER, PERMISSION_FIND_TABLE); + AccessControlGroup cook = group(GROUP_COOK, readMasterData, PERMISSION_FIND_ORDER, PERMISSION_SAVE_ORDER, + PERMISSION_FIND_ORDER_POSITION, PERMISSION_SAVE_ORDER_POSITION); + AccessControlGroup barkeeper = group(GROUP_BARKEEPER, cook, PERMISSION_FIND_BILL, PERMISSION_SAVE_BILL, + PERMISSION_DELETE_BILL, PERMISSION_DELETE_ORDER); + AccessControlGroup waiter = group(GROUP_WAITER, barkeeper, PERMISSION_SAVE_TABLE); + // AccessControlGroup chief = + group(GROUP_CHIEF, waiter, PERMISSION_SAVE_OFFER, PERMISSION_SAVE_PRODUCT, PERMISSION_SAVE_STAFF_MEMBER, + PERMISSION_DELETE_OFFER, PERMISSION_DELETE_PRODUCT, PERMISSION_DELETE_STAFF_MEMBER, + PERMISSION_DELETE_ORDER_POSITION, PERMISSION_DELETE_TABLE); + } + +} \ No newline at end of file diff --git a/modules/security/src/test/java/com/devonfw/module/security/common/impl/accesscontrol/AccessControlConfigTest.java b/modules/security/src/test/java/com/devonfw/module/security/common/impl/accesscontrol/AccessControlConfigTest.java new file mode 100644 index 00000000..06d0280e --- /dev/null +++ b/modules/security/src/test/java/com/devonfw/module/security/common/impl/accesscontrol/AccessControlConfigTest.java @@ -0,0 +1,147 @@ +package com.devonfw.module.security.common.impl.accesscontrol; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.annotation.security.RolesAllowed; + +import org.junit.Test; + +import com.devonfw.module.security.common.api.accesscontrol.AccessControl; +import com.devonfw.module.security.common.api.accesscontrol.AccessControlGroup; +import com.devonfw.module.security.common.base.accesscontrol.AccessControlConfig; +import com.devonfw.module.test.common.base.ModuleTest; + +/** + * Test of {@link AccessControlConfig}. + */ +public class AccessControlConfigTest extends ModuleTest { + + /** + * Test of {@link AccessControlConfig#getAccessControl(String)} with top-level chief group. + */ + @Test + // @RolesAllowed only used to ensure that the constant can be referenced here + @RolesAllowed(AccessControlConfigSimple.PERMISSION_FIND_TABLE) + public void testGetAccessControl() { + + // given + AccessControlConfigSimple config = new AccessControlConfigSimple(); + String groupChief = AccessControlConfigSimple.GROUP_CHIEF; + + // when + AccessControlGroup chief = (AccessControlGroup) config.getAccessControl(groupChief); + + // then + assertThat(chief).isNotNull(); + assertThat(chief.getId()).isEqualTo(groupChief); + assertThat(flatten(chief.getPermissions())).containsExactlyInAnyOrder( + AccessControlConfigSimple.PERMISSION_SAVE_OFFER, AccessControlConfigSimple.PERMISSION_SAVE_PRODUCT, + AccessControlConfigSimple.PERMISSION_SAVE_STAFF_MEMBER, AccessControlConfigSimple.PERMISSION_DELETE_OFFER, + AccessControlConfigSimple.PERMISSION_DELETE_PRODUCT, AccessControlConfigSimple.PERMISSION_DELETE_STAFF_MEMBER, + AccessControlConfigSimple.PERMISSION_DELETE_ORDER_POSITION, AccessControlConfigSimple.PERMISSION_DELETE_TABLE); + AccessControlGroup waiter = getSingleInherit(chief); + assertThat(waiter).isNotNull(); + assertThat(waiter.getId()).isEqualTo(AccessControlConfigSimple.GROUP_WAITER); + assertThat(flatten(waiter.getPermissions())) + .containsExactlyInAnyOrder(AccessControlConfigSimple.PERMISSION_SAVE_TABLE); + AccessControlGroup barkeeper = getSingleInherit(waiter); + assertThat(barkeeper).isNotNull(); + assertThat(barkeeper.getId()).isEqualTo(AccessControlConfigSimple.GROUP_BARKEEPER); + assertThat(flatten(barkeeper.getPermissions())).containsExactlyInAnyOrder( + AccessControlConfigSimple.PERMISSION_FIND_BILL, AccessControlConfigSimple.PERMISSION_SAVE_BILL, + AccessControlConfigSimple.PERMISSION_DELETE_BILL, AccessControlConfigSimple.PERMISSION_DELETE_ORDER); + AccessControlGroup cook = getSingleInherit(barkeeper); + assertThat(cook).isNotNull(); + assertThat(cook.getId()).isEqualTo(AccessControlConfigSimple.GROUP_COOK); + assertThat(flatten(cook.getPermissions())).containsExactlyInAnyOrder( + AccessControlConfigSimple.PERMISSION_FIND_ORDER, AccessControlConfigSimple.PERMISSION_SAVE_ORDER, + AccessControlConfigSimple.PERMISSION_FIND_ORDER_POSITION, + AccessControlConfigSimple.PERMISSION_SAVE_ORDER_POSITION); + AccessControlGroup readMasterData = getSingleInherit(cook); + assertThat(readMasterData).isNotNull(); + assertThat(readMasterData.getId()).isEqualTo(AccessControlConfigSimple.GROUP_READ_MASTER_DATA); + assertThat(flatten(readMasterData.getPermissions())).containsExactlyInAnyOrder( + AccessControlConfigSimple.PERMISSION_FIND_OFFER, AccessControlConfigSimple.PERMISSION_FIND_PRODUCT, + AccessControlConfigSimple.PERMISSION_FIND_STAFF_MEMBER, AccessControlConfigSimple.PERMISSION_FIND_TABLE); + assertThat(readMasterData.getInherits()).isEmpty(); + } + + /** + * Test of {@link AccessControlConfig#collectAccessControlIds(String, Set)} with + * {@link AccessControlConfigSimple#GROUP_READ_MASTER_DATA}. + */ + @Test + public void testCollectAccessControlIds4ReadMasterData() { + + // given + AccessControlConfigSimple config = new AccessControlConfigSimple(); + + // when + Set permissions = new HashSet<>(); + config.collectAccessControlIds(AccessControlConfigSimple.GROUP_READ_MASTER_DATA, permissions); + + // then + assertThat(permissions).containsExactlyInAnyOrder(AccessControlConfigSimple.GROUP_READ_MASTER_DATA, + AccessControlConfigSimple.PERMISSION_FIND_OFFER, AccessControlConfigSimple.PERMISSION_FIND_PRODUCT, + AccessControlConfigSimple.PERMISSION_FIND_STAFF_MEMBER, AccessControlConfigSimple.PERMISSION_FIND_TABLE); + } + + /** + * Test of {@link AccessControlConfig#collectAccessControlIds(String, Set)} with + * {@link AccessControlConfigSimple#GROUP_CHIEF}. + */ + @Test + public void testCollectAccessControlIds4Chief() { + + // given + AccessControlConfigSimple config = new AccessControlConfigSimple(); + String groupChief = AccessControlConfigSimple.GROUP_CHIEF; + + // when + Set permissions = new HashSet<>(); + config.collectAccessControlIds(groupChief, permissions); + + // then + assertThat(permissions).containsExactlyInAnyOrder(AccessControlConfigSimple.GROUP_READ_MASTER_DATA, + AccessControlConfigSimple.PERMISSION_FIND_OFFER, AccessControlConfigSimple.PERMISSION_FIND_PRODUCT, + AccessControlConfigSimple.PERMISSION_FIND_STAFF_MEMBER, AccessControlConfigSimple.PERMISSION_FIND_TABLE, + // + AccessControlConfigSimple.GROUP_COOK, AccessControlConfigSimple.PERMISSION_FIND_ORDER, + AccessControlConfigSimple.PERMISSION_SAVE_ORDER, AccessControlConfigSimple.PERMISSION_FIND_ORDER_POSITION, + AccessControlConfigSimple.PERMISSION_SAVE_ORDER_POSITION, + // + AccessControlConfigSimple.GROUP_BARKEEPER, AccessControlConfigSimple.PERMISSION_FIND_BILL, + AccessControlConfigSimple.PERMISSION_SAVE_BILL, AccessControlConfigSimple.PERMISSION_DELETE_BILL, + AccessControlConfigSimple.PERMISSION_DELETE_ORDER, + // + AccessControlConfigSimple.GROUP_WAITER, AccessControlConfigSimple.PERMISSION_SAVE_TABLE, + // + groupChief, AccessControlConfigSimple.PERMISSION_SAVE_OFFER, AccessControlConfigSimple.PERMISSION_SAVE_PRODUCT, + AccessControlConfigSimple.PERMISSION_SAVE_STAFF_MEMBER, AccessControlConfigSimple.PERMISSION_DELETE_OFFER, + AccessControlConfigSimple.PERMISSION_DELETE_PRODUCT, AccessControlConfigSimple.PERMISSION_DELETE_STAFF_MEMBER, + AccessControlConfigSimple.PERMISSION_DELETE_ORDER_POSITION, AccessControlConfigSimple.PERMISSION_DELETE_TABLE); + } + + private static AccessControlGroup getSingleInherit(AccessControlGroup group) { + + List inherits = group.getInherits(); + assertThat(inherits).hasSize(1); + AccessControlGroup inheritedGroup = inherits.get(0); + assertThat(inheritedGroup).isNotNull(); + return inheritedGroup; + } + + private static String[] flatten(Collection accessControlList) { + + String[] ids = new String[accessControlList.size()]; + int i = 0; + for (AccessControl accessControl : accessControlList) { + ids[i++] = accessControl.getId(); + } + return ids; + } + +} diff --git a/modules/security/src/test/java/com/devonfw/module/security/common/impl/accesscontrol/AccessControlSchemaTest.java b/modules/security/src/test/java/com/devonfw/module/security/common/impl/accesscontrol/AccessControlSchemaTest.java new file mode 100644 index 00000000..1ac48ed5 --- /dev/null +++ b/modules/security/src/test/java/com/devonfw/module/security/common/impl/accesscontrol/AccessControlSchemaTest.java @@ -0,0 +1,265 @@ +package com.devonfw.module.security.common.impl.accesscontrol; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.core.io.ClassPathResource; + +import com.devonfw.module.security.common.api.accesscontrol.AccessControl; +import com.devonfw.module.security.common.api.accesscontrol.AccessControlGroup; +import com.devonfw.module.security.common.api.accesscontrol.AccessControlPermission; +import com.devonfw.module.security.common.api.accesscontrol.AccessControlProvider; +import com.devonfw.module.security.common.api.accesscontrol.AccessControlSchema; +import com.devonfw.module.security.common.impl.accesscontrol.AccessControlProviderImpl; +import com.devonfw.module.security.common.impl.accesscontrol.AccessControlSchemaProviderImpl; +import com.devonfw.module.security.common.impl.accesscontrol.AccessControlSchemaXmlMapper; +import com.devonfw.module.test.common.base.ModuleTest; + +/** + * This is the test-case for {@link AccessControlSchema} and {@link AccessControlSchemaXmlMapper}. + * + */ +public class AccessControlSchemaTest extends ModuleTest { + + /** The location of the reference configuration for regression tests. */ + private static final String SCHEMA_XML = "config/app/security/access-control-schema.xml"; + + /** The location of the reference configuration with group type declaration */ + private static final String SCHEMA_XML_GROUP_TYPES = "config/app/security/access-control-schema_groupTypes.xml"; + + /** The location of the configuration with a cyclic dependency. */ + private static final String SCHEMA_XML_CYCLIC = "config/app/security/access-control-schema_cyclic.xml"; + + /** The location of the configuration that is syntactically corrupted (invalid group reference). */ + private static final String SCHEMA_XML_CORRUPTED = "config/app/security/access-control-schema_corrupted.xml"; + + /** The location of the configuration that is syntactically corrupted (invalid group reference). */ + private static final String SCHEMA_XML_ILLEGAL = "config/app/security/access-control-schema_illegal.xml"; + + /** + * The constructor. + */ + public AccessControlSchemaTest() { + + super(); + } + + /** + * Regression test for {@link AccessControlSchemaXmlMapper#write(AccessControlSchema, java.io.OutputStream)}. + * + * @throws Exception if something goes wrong. + */ + @Test + public void testWriteXml() throws Exception { + + // given + AccessControlSchema conf = createSecurityConfiguration(); + String expectedXml = readSecurityConfigurationXmlFile(); + // when + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + new AccessControlSchemaXmlMapper().write(conf, baos); + String actualXml = baos.toString(); + // then + assertThat(expectedXml.replaceAll("\\r *|\\n *", "")).isEqualTo(actualXml); + } + + /** + * Regression test for {@link AccessControlSchemaXmlMapper#read(InputStream)}. + * + * @throws Exception if something goes wrong. + */ + @Test + public void testReadXml() throws Exception { + + // given + AccessControlSchema expectedConf = createSecurityConfiguration(); + // when + ClassPathResource resource = new ClassPathResource(SCHEMA_XML); + AccessControlSchema actualConf; + try (InputStream in = resource.getInputStream()) { + actualConf = new AccessControlSchemaXmlMapper().read(in); + } + // then + assertThat(expectedConf).isEqualTo(actualConf); + } + + /** + * Tests that {@link AccessControlProviderImpl} properly detects cyclic inheritance of {@link AccessControlGroup}s. + */ + @Test + public void testProviderValid() { + + createProvider(SCHEMA_XML); + } + + /** + * Tests that {@link AccessControlProviderImpl} properly detects cyclic inheritance of {@link AccessControlGroup}s. + */ + @Test + public void testProviderCyclic() { + + try { + createProvider(SCHEMA_XML_CYCLIC); + fail("Exception expected!"); + } catch (Exception e) { + assertThat(e).hasMessageContaining("Cook-->Chief-->Barkeeper"); + } + } + + /** Tests that {@link AccessControlProviderImpl} with corrupted XML (not well-formed). */ + @Test + public void testProviderCorrupted() { + + try { + createProvider(SCHEMA_XML_CORRUPTED); + fail("Exception expected!"); + } catch (IllegalStateException e) { + String message = e.getMessage(); + assertThat(message).contains(SCHEMA_XML_CORRUPTED.toString()); + String causeMessage = e.getCause().getMessage(); + assertThat("Unmarshalling XML failed!").isEqualToIgnoringCase(causeMessage); + } + } + + /** Tests that {@link AccessControlProviderImpl} with illegal XML (undefined group reference). */ + @Test + public void testProviderIllegal() { + + try { + createProvider(SCHEMA_XML_ILLEGAL); + fail("Exception expected!"); + } catch (IllegalStateException e) { + String message = e.getMessage(); + assertThat(message).contains(SCHEMA_XML_ILLEGAL.toString()); + String causeMessage = e.getCause().getMessage(); + assertThat(causeMessage).contains("Undefined ID \"Waiter\""); + } + } + + /** + * Tests that {@link AccessControlProviderImpl} properly detects cyclic inheritance of {@link AccessControlGroup}s. + */ + public void testProvider() { + + AccessControlProvider provider = createProvider(SCHEMA_XML); + Set permissions = new HashSet<>(); + boolean success; + success = provider.collectAccessControls("", permissions); + assertThat(success).isFalse(); + assertThat(permissions.size()).isEqualTo(0); + success = provider.collectAccessControls("Admin", permissions); + assertThat(success).isTrue(); + assertThat(permissions).contains(provider.getAccessControl("Customer_ReadCustomer")); + assertThat(permissions).contains(provider.getAccessControl("Customer_CreateCustomer")); + assertThat(permissions).contains(provider.getAccessControl("Customer_DeleteCustomer")); + assertThat(permissions.size()).isEqualTo(24); + success = provider.collectAccessControls("ReadOnly", permissions); + assertThat(success).isTrue(); + assertThat(permissions).contains(provider.getAccessControl("Contract_ReadContractAsset")); + assertThat(permissions).contains(provider.getAccessControl("Contract_UpdateContractAsset")); + assertThat(permissions).doesNotContain(provider.getAccessControl("System_DeleteUser")); + assertThat(permissions.size()).isEqualTo(5); + + } + + /** Tests the correct extraction of group types */ + @Test + public void testGroupTypes() { + + ClassPathResource resource = new ClassPathResource(SCHEMA_XML_GROUP_TYPES); + AccessControlSchemaProviderImpl accessControlSchemaProvider = new AccessControlSchemaProviderImpl(); + accessControlSchemaProvider.setAccessControlSchema(resource); + AccessControlSchema accessControlSchema = accessControlSchemaProvider.loadSchema(); + List groups = accessControlSchema.getGroups(); + + Assert.assertNotNull(groups); + Assert.assertEquals(3, groups.size()); + + for (AccessControlGroup group : groups) { + if (group.getId().equals("Admin")) { + Assert.assertEquals("role", group.getType()); + } else if (group.getId().equals("ReadOnly") || group.getId().equals("ReadWrite")) { + Assert.assertEquals("group", group.getType()); + } + } + } + + private AccessControlProvider createProvider(String location) { + + ClassPathResource resource = new ClassPathResource(location); + AccessControlProviderImpl accessControlProvider = new AccessControlProviderImpl(); + AccessControlSchemaProviderImpl accessControlSchemaProvider = new AccessControlSchemaProviderImpl(); + accessControlSchemaProvider.setAccessControlSchema(resource); + accessControlProvider.setAccessControlSchemaProvider(accessControlSchemaProvider); + accessControlProvider.initialize(); + return accessControlProvider; + } + + private String readSecurityConfigurationXmlFile() throws IOException, UnsupportedEncodingException { + + ClassPathResource resource = new ClassPathResource(SCHEMA_XML); + byte[] data = Files.readAllBytes(Paths.get(resource.getURI())); + String expectedXml = new String(data, "UTF-8"); + return expectedXml; + } + + private AccessControlSchema createSecurityConfiguration() { + + AccessControlSchema conf = new AccessControlSchema(); + AccessControlGroup readOnly = new AccessControlGroup("ReadOnly"); + readOnly.getPermissions().add(new AccessControlPermission("Customer_ReadCustomer")); + readOnly.getPermissions().add(new AccessControlPermission("Customer_ReadProfile")); + readOnly.getPermissions().add(new AccessControlPermission("Customer_ReadAddress")); + readOnly.getPermissions().add(new AccessControlPermission("Contract_ReadContract")); + readOnly.getPermissions().add(new AccessControlPermission("Contract_ReadContractAsset")); + AccessControlGroup readWrite = new AccessControlGroup("ReadWrite"); + readWrite.getInherits().add(readOnly); + readWrite.getPermissions().add(new AccessControlPermission("Customer_CreateCustomer")); + readWrite.getPermissions().add(new AccessControlPermission("Customer_CreateProfile")); + readWrite.getPermissions().add(new AccessControlPermission("Customer_CreateAddress")); + readWrite.getPermissions().add(new AccessControlPermission("Contract_CreateContract")); + readWrite.getPermissions().add(new AccessControlPermission("Contract_CreateContractAsset")); + readWrite.getPermissions().add(new AccessControlPermission("Customer_UpdateCustomer")); + readWrite.getPermissions().add(new AccessControlPermission("Customer_UpdateProfile")); + readWrite.getPermissions().add(new AccessControlPermission("Customer_UpdateAddress")); + readWrite.getPermissions().add(new AccessControlPermission("Contract_UpdateContract")); + readWrite.getPermissions().add(new AccessControlPermission("Contract_UpdateContractAsset")); + AccessControlGroup customerAdmin = new AccessControlGroup("CustomerAdmin"); + customerAdmin.getInherits().add(readWrite); + customerAdmin.getPermissions().add(new AccessControlPermission("Customer_DeleteCustomer")); + customerAdmin.getPermissions().add(new AccessControlPermission("Customer_DeleteProfile")); + customerAdmin.getPermissions().add(new AccessControlPermission("Customer_DeleteAddress")); + AccessControlGroup contractAdmin = new AccessControlGroup("ContractAdmin"); + contractAdmin.getInherits().add(readWrite); + contractAdmin.getPermissions().add(new AccessControlPermission("Contract_DeleteContract")); + contractAdmin.getPermissions().add(new AccessControlPermission("Contract_DeleteContractAsset")); + AccessControlGroup systemAdmin = new AccessControlGroup("SystemAdmin"); + systemAdmin.getInherits().add(readWrite); + systemAdmin.getPermissions().add(new AccessControlPermission("System_ReadUser")); + systemAdmin.getPermissions().add(new AccessControlPermission("System_CreateUser")); + systemAdmin.getPermissions().add(new AccessControlPermission("System_UpdateUser")); + systemAdmin.getPermissions().add(new AccessControlPermission("System_DeleteUser")); + AccessControlGroup admin = new AccessControlGroup("Admin"); + admin.getInherits().add(customerAdmin); + admin.getInherits().add(contractAdmin); + admin.getInherits().add(systemAdmin); + admin.getPermissions(); + conf.getGroups().add(readOnly); + conf.getGroups().add(readWrite); + conf.getGroups().add(customerAdmin); + conf.getGroups().add(contractAdmin); + conf.getGroups().add(systemAdmin); + conf.getGroups().add(admin); + return conf; + } + +} diff --git a/modules/security/src/test/resources/config/app/security/access-control-schema.xml b/modules/security/src/test/resources/config/app/security/access-control-schema.xml new file mode 100644 index 00000000..fa7de0bb --- /dev/null +++ b/modules/security/src/test/resources/config/app/security/access-control-schema.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + ReadOnly + + + + + + + + + + + + + + + + + ReadWrite + + + + + + + + + + ReadWrite + + + + + + + + + ReadWrite + + + + + + + + + + + CustomerAdmin + ContractAdmin + SystemAdmin + + + + \ No newline at end of file diff --git a/modules/security/src/test/resources/config/app/security/access-control-schema_corrupted.xml b/modules/security/src/test/resources/config/app/security/access-control-schema_corrupted.xml new file mode 100644 index 00000000..63b25fe5 --- /dev/null +++ b/modules/security/src/test/resources/config/app/security/access-control-schema_corrupted.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/modules/security/src/test/resources/config/app/security/access-control-schema_cyclic.xml b/modules/security/src/test/resources/config/app/security/access-control-schema_cyclic.xml new file mode 100644 index 00000000..c6900fb0 --- /dev/null +++ b/modules/security/src/test/resources/config/app/security/access-control-schema_cyclic.xml @@ -0,0 +1,28 @@ + + + + + + + + + + Chief + + + + + Barkeeper + + + + + Waiter + Cook + + + + + + + diff --git a/modules/security/src/test/resources/config/app/security/access-control-schema_groupTypes.xml b/modules/security/src/test/resources/config/app/security/access-control-schema_groupTypes.xml new file mode 100644 index 00000000..7dfd6f70 --- /dev/null +++ b/modules/security/src/test/resources/config/app/security/access-control-schema_groupTypes.xml @@ -0,0 +1,24 @@ + + + + + + + + + + ReadOnly + + + + + + + + ReadWrite + + + + + + \ No newline at end of file diff --git a/modules/security/src/test/resources/config/app/security/access-control-schema_illegal.xml b/modules/security/src/test/resources/config/app/security/access-control-schema_illegal.xml new file mode 100644 index 00000000..fd4a6a9f --- /dev/null +++ b/modules/security/src/test/resources/config/app/security/access-control-schema_illegal.xml @@ -0,0 +1,8 @@ + + + + + Waiter + + + diff --git a/modules/service/pom.xml b/modules/service/pom.xml new file mode 100644 index 00000000..d06db37c --- /dev/null +++ b/modules/service/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-modules + dev-SNAPSHOT + + com.devonfw.java.modules + devon4j-service + ${devon4j.version} + jar + ${project.artifactId} + Service Support Module of the Open Application Standard Platform for Java (devon4j). + + + + org.slf4j + slf4j-api + + + javax.inject + javax.inject + + + ${project.groupId} + devon4j-basic + + + ${project.groupId} + devon4j-logging + true + + + org.springframework.security + spring-security-core + true + + + org.springframework.boot + spring-boot + true + + + javax.annotation + javax.annotation-api + test + + + + diff --git a/modules/service/src/main/java/com/devonfw/module/service/common/api/Service.java b/modules/service/src/main/java/com/devonfw/module/service/common/api/Service.java new file mode 100644 index 00000000..c6263edb --- /dev/null +++ b/modules/service/src/main/java/com/devonfw/module/service/common/api/Service.java @@ -0,0 +1,16 @@ +package com.devonfw.module.service.common.api; + +/** + * This is a marker interface for a remote service. Such service if offered by an application and can be called + * from other applications via the network. Such services often use HTTP (such as REST services or SOAP services) but + * may also use other protocols. It is recommended that you define the API of the service via an interface and then + * provide the implementation as a class implementing that interface. You are not forced to extend this marker interface + * by your service API interface but doing so gives you some advantages like auto-registration of your services when + * using the according spring-boot-starter with zero configuration. If you want to decouple your code as much as + * possible you are free to ignore this interface or simply copy it to your own project and package. + * + * @since 3.0.0 + */ +public interface Service { + +} diff --git a/modules/service/src/main/java/com/devonfw/module/service/common/api/client/ServiceClientFactory.java b/modules/service/src/main/java/com/devonfw/module/service/common/api/client/ServiceClientFactory.java new file mode 100644 index 00000000..5b960b4a --- /dev/null +++ b/modules/service/src/main/java/com/devonfw/module/service/common/api/client/ServiceClientFactory.java @@ -0,0 +1,61 @@ +package com.devonfw.module.service.common.api.client; + +import java.util.Map; + +import com.devonfw.module.service.common.api.Service; + +/** + * This is the interface for a factory used to {@link #create(Class) create} client stubs for a {@link Service}. The + * following example shows the typical usage in your code: + * + *
+ * @{@link javax.inject.Named}
+ * public class UcMyUseCaseImpl extends MyUseCaseBase implements UcMyUseCase {
+ *   @{@link javax.inject.Inject} private {@link ServiceClientFactory} clientFactory;
+ *
+ *   @{@link Override} @{@link javax.annotation.security.RolesAllowed}(...)
+ *   public Foo doSomething(Bar bar) {
+ *     MyExternalServiceApi externalService = this.clientFactory.{@link ServiceClientFactory#create(Class) create}(MyExternalServiceApi.class);
+ *     Some result = externalService.doSomething(convert(bar));
+ *     return convert(result);
+ *   }
+ * }
+ * 
+ * + * As you can see creating a service client stub is easy and requires only a single line of code. However, internally a + * lot of things happen such as the following aspects: + *
    + *
  • {@link com.devonfw.module.service.common.api.client.discovery.ServiceDiscoverer#discover(com.devonfw.module.service.common.api.client.discovery.ServiceDiscoveryContext) + * service discovery}.
  • + *
  • {@link com.devonfw.module.service.common.api.header.ServiceHeaderCustomizer#addHeaders(com.devonfw.module.service.common.api.header.ServiceHeaderContext) + * header customization} (for security, correlation-ID, etc.).
  • + *
  • performance logging
  • + *
  • exception mapping (exception facade)
  • + *
+ * All these aspects can be configured via spring and customized with own implementations. + * + * @since 3.0.0 + */ +public interface ServiceClientFactory { + + /** + * @param the generic type of the {@code serviceInterface}. For flexibility and being not invasive this generic is + * not bound to {@link Service} ({@code S extends Service}). + * @param serviceInterface the {@link Class} reflecting the interface that defines the API of your {@link Service}. + * @return a new instance of the given {@code serviceInterface} that is a client stub. Invocations to any of the + * service methods will trigger a remote call and synchronously return the result. + */ + S create(Class serviceInterface); + + /** + * @param the generic type of the {@code serviceInterface}. For flexibility and being not invasive this generic is + * not bound to {@link Service} ({@code S extends Service}). + * @param serviceInterface the {@link Class} reflecting the interface that defines the API of your {@link Service}. + * @param config the {@link Map} with explicit configuration properties. See + * {@link com.devonfw.module.service.common.base.config.ServiceConfigProperties} for further details. + * @return a new instance of the given {@code serviceInterface} that is a client stub. Invocations to any of the + * service methods will trigger a remote call and synchronously return the result. + */ + S create(Class serviceInterface, Map config); + +} diff --git a/modules/service/src/main/java/com/devonfw/module/service/common/api/client/config/ServiceClientConfigBuilder.java b/modules/service/src/main/java/com/devonfw/module/service/common/api/client/config/ServiceClientConfigBuilder.java new file mode 100644 index 00000000..3f8af620 --- /dev/null +++ b/modules/service/src/main/java/com/devonfw/module/service/common/api/client/config/ServiceClientConfigBuilder.java @@ -0,0 +1,187 @@ +package com.devonfw.module.service.common.api.client.config; + +import java.util.HashMap; +import java.util.Map; + +import com.devonfw.module.basic.common.api.config.ConfigProperties; +import com.devonfw.module.basic.common.api.config.SimpleConfigProperties; +import com.devonfw.module.service.common.api.config.ServiceConfig; + +/** + * A builder used to create the configuration for {@link com.devonfw.module.service.common.api.Service} clients. + * + * @see com.devonfw.module.service.common.api.client.ServiceClientFactory#create(Class, Map) + */ +public class ServiceClientConfigBuilder { + + private final Map map; + + /** + * The constructor. + */ + public ServiceClientConfigBuilder() { + super(); + this.map = new HashMap<>(); + } + + /** + * Use HTTP (without encryption). + * + * @return this instance for fluent API calls. + */ + public ServiceClientConfigBuilder protocolHttp() { + + return protocol("http"); + } + + /** + * Use HTTPS (with TLS). + * + * @return this instance for fluent API calls. + */ + public ServiceClientConfigBuilder protocolHttps() { + + return protocol("https"); + } + + /** + * @param protocol the protocol to use. + * @return this instance for fluent API calls. + */ + public ServiceClientConfigBuilder protocol(String protocol) { + + this.map.put(ServiceConfig.KEY_SEGMENT_PROTOCOL, protocol); + return this; + } + + /** + * @param port the port-number used to build the {@link #url(String) URL}. + * @return this instance for fluent API calls. + */ + public ServiceClientConfigBuilder port(int port) { + + this.map.put(ServiceConfig.KEY_SEGMENT_PORT, Integer.toString(port)); + return this; + } + + /** + * @param host the host (name or IP) used to build the {@link #url(String) URL}. + * @return this instance for fluent API calls. + */ + public ServiceClientConfigBuilder host(String host) { + + this.map.put(ServiceConfig.KEY_SEGMENT_HOST, host); + return this; + } + + /** + * @param url the entire URL of the {@link com.devonfw.module.service.common.api.Service}. + * @return this instance for fluent API calls. + */ + public ServiceClientConfigBuilder url(String url) { + + this.map.put(ServiceConfig.KEY_SEGMENT_URL, url); + return this; + } + + /** + * Use basic {@link #auth(String) authentication}. + * + * @return this instance for fluent API calls. + */ + public ServiceClientConfigBuilder authBasic() { + + return auth(ServiceConfig.VALUE_AUTH_BASIC); + } + + /** + * Use OAuth {@link #auth(String) authentication}. + * + * @return this instance for fluent API calls. + */ + public ServiceClientConfigBuilder authOAuth() { + + return auth(ServiceConfig.VALUE_AUTH_OAUTH); + } + + /** + * Use OAuth {@link #auth(String) authentication}. + * + * @return this instance for fluent API calls. + */ + public ServiceClientConfigBuilder authForward() { + + return auth(ServiceConfig.VALUE_AUTH_FORWARD); + } + + /** + * @param authentication the {@link ServiceConfig#KEY_SEGMENT_AUTH authentication} type. + * @return this instance for fluent API calls. + */ + public ServiceClientConfigBuilder auth(String authentication) { + + this.map.put(ServiceConfig.KEY_SEGMENT_AUTH, authentication); + return this; + } + + /** + * @param login the {@link ServiceConfig#KEY_SEGMENT_USER_LOGIN login} of the {@link ServiceConfig#KEY_SEGMENT_USER + * user} for authentication. + * @return this instance for fluent API calls. + */ + public ServiceClientConfigBuilder userLogin(String login) { + + this.map.put(ServiceConfig.KEY_SEGMENT_USER + "." + ServiceConfig.KEY_SEGMENT_USER_LOGIN, login); + return this; + } + + /** + * @param password the {@link ServiceConfig#KEY_SEGMENT_USER_PASSWORD password} of the + * {@link ServiceConfig#KEY_SEGMENT_USER user} for authentication. + * @return this instance for fluent API calls. + */ + public ServiceClientConfigBuilder userPassword(String password) { + + this.map.put(ServiceConfig.KEY_SEGMENT_USER + "." + ServiceConfig.KEY_SEGMENT_USER_PASSWORD, password); + return this; + } + + /** + * @param timeout the {@link ServiceConfig#KEY_SEGMENT_TIMEOUT_CONNECTION connecion timeout} in seconds. + * @return this instance for fluent API calls. + */ + public ServiceClientConfigBuilder timeoutConnection(long timeout) { + + this.map.put(ServiceConfig.KEY_SEGMENT_TIMEOUT + "." + ServiceConfig.KEY_SEGMENT_TIMEOUT_CONNECTION, + Long.toString(timeout)); + return this; + } + + /** + * @param timeout the {@link ServiceConfig#KEY_SEGMENT_TIMEOUT_RESPONSE response timeout} in seconds. + * @return this instance for fluent API calls. + */ + public ServiceClientConfigBuilder timeoutResponse(long timeout) { + + this.map.put(ServiceConfig.KEY_SEGMENT_TIMEOUT + "." + ServiceConfig.KEY_SEGMENT_TIMEOUT_RESPONSE, + Long.toString(timeout)); + return this; + } + + /** + * @return the current configuration as flat {@link Map}. + */ + public Map buildMap() { + + return this.map; + } + + /** + * @return the current configuration as {@link ConfigProperties}. + */ + public ConfigProperties buildConfigProperties() { + + return SimpleConfigProperties.ofFlatMap(this.map); + } + +} diff --git a/modules/service/src/main/java/com/devonfw/module/service/common/api/client/context/ServiceContext.java b/modules/service/src/main/java/com/devonfw/module/service/common/api/client/context/ServiceContext.java new file mode 100644 index 00000000..31842b72 --- /dev/null +++ b/modules/service/src/main/java/com/devonfw/module/service/common/api/client/context/ServiceContext.java @@ -0,0 +1,43 @@ +package com.devonfw.module.service.common.api.client.context; + +import java.util.Collection; + +import com.devonfw.module.basic.common.api.config.ConfigProperties; + +/** + * This interface gives read access to contextual information of a {@link com.devonfw.module.service.common.api.Service}. + * + * @param the generic type of the {@link #getApi() service API}. + * + * @since 3.0.0 + */ +public interface ServiceContext { + + /** + * @return the {@link Class} reflecting the API of the {@link com.devonfw.module.service.common.api.Service}. + */ + Class getApi(); + + /** + * @return the URL (or URI) of the remote service. + */ + String getUrl(); + + /** + * @return a {@link Collection} with the available {@link #getHeader(String) header} names (keys). + */ + Collection getHeaderNames(); + + /** + * @param name the name (key) of the header to get. + * @return the value of the requested header or {@code null} if undefined. + */ + String getHeader(String name); + + /** + * @return the {@link ConfigProperties} with configuration metadata. + * @see com.devonfw.module.service.common.api.client.ServiceClientFactory#create(Class, java.util.Map) + */ + ConfigProperties getConfig(); + +} diff --git a/modules/service/src/main/java/com/devonfw/module/service/common/api/client/discovery/ServiceDiscoverer.java b/modules/service/src/main/java/com/devonfw/module/service/common/api/client/discovery/ServiceDiscoverer.java new file mode 100644 index 00000000..88807d89 --- /dev/null +++ b/modules/service/src/main/java/com/devonfw/module/service/common/api/client/discovery/ServiceDiscoverer.java @@ -0,0 +1,29 @@ +package com.devonfw.module.service.common.api.client.discovery; + +/** + * This interface abstracts the aspect of the {@link #discover(ServiceDiscoveryContext) discovery} of a + * {@link com.devonfw.module.service.common.api.Service}. You may choose an exiting implementation or write your own to + * customize the {@link #discover(ServiceDiscoveryContext) discovery} of your + * {@link com.devonfw.module.service.common.api.Service}s. + * + * @since 3.0.0 + */ +public interface ServiceDiscoverer { + + /** + * @param context the {@link ServiceDiscoveryContext} where to + * {@link ServiceDiscoveryContext#setConfig(com.devonfw.module.basic.common.api.config.ConfigProperties) + * set the discovered configuration}. At least the {@link ServiceDiscoveryContext#getUrl() URL} has to be + * discovered.
+ * It is possible to have multiple implementations of this interface as spring beans in your context. An + * implementation may decide that it is not responsible for the given {@link ServiceDiscoveryContext#getApi() + * service API} (e.g. only responsible for REST services). In that case it can return without doing any + * modifications to the given {@link ServiceDiscoveryContext}. Until the + * {@link ServiceDiscoveryContext#getUrl() URL} has not been discovered further implementations of this + * interface will be {@link #discover(ServiceDiscoveryContext) invoked}. If all implementations have been + * invoked without discovering the {@link ServiceDiscoveryContext#getUrl() URL}, discovery will fail with a + * runtime exception causing the {@link com.devonfw.module.service.common.api.client.ServiceClientFactory} to fail. + */ + void discover(ServiceDiscoveryContext context); + +} diff --git a/modules/service/src/main/java/com/devonfw/module/service/common/api/client/discovery/ServiceDiscoveryContext.java b/modules/service/src/main/java/com/devonfw/module/service/common/api/client/discovery/ServiceDiscoveryContext.java new file mode 100644 index 00000000..2bfb18bb --- /dev/null +++ b/modules/service/src/main/java/com/devonfw/module/service/common/api/client/discovery/ServiceDiscoveryContext.java @@ -0,0 +1,24 @@ +package com.devonfw.module.service.common.api.client.discovery; + +import com.devonfw.module.basic.common.api.config.ConfigProperties; +import com.devonfw.module.service.common.api.client.context.ServiceContext; + +/** + * Extends {@link ServiceContext} and allows to {@link #setConfig(ConfigProperties) update the configuration + * properties}. + * + * @param the generic type of the {@link #getApi() service API}. + * + * @since 3.0.0 + */ +public interface ServiceDiscoveryContext extends ServiceContext { + + /** + * @param config the {@link ServiceDiscoverer#discover(ServiceDiscoveryContext) discovered} + * {@link com.devonfw.module.service.common.api.Service} configuration such as {@link #getUrl() URL}. Has to be + * {@link ConfigProperties#inherit(ConfigProperties) inherited} from {@link #getConfig() existing + * configuration properties}. + */ + void setConfig(ConfigProperties config); + +} diff --git a/modules/service/src/main/java/com/devonfw/module/service/common/api/config/ServiceConfig.java b/modules/service/src/main/java/com/devonfw/module/service/common/api/config/ServiceConfig.java new file mode 100644 index 00000000..78e60cb2 --- /dev/null +++ b/modules/service/src/main/java/com/devonfw/module/service/common/api/config/ServiceConfig.java @@ -0,0 +1,97 @@ +package com.devonfw.module.service.common.api.config; + +import com.devonfw.module.basic.common.api.config.ConfigProperties; +import com.devonfw.module.service.common.base.config.ServiceConfigProperties; + +/** + * Configuration for {@link com.devonfw.module.service.common.api.client.ServiceClientFactory} and all related + * implementations. + * + * @see ServiceConfigProperties + * + * @since 3.0.0 + */ +public interface ServiceConfig { + + /** The key segment for the client specific configuration sub-tree. */ + String KEY_SEGMENT_CLIENT = "client"; + + /** The key segment for the URL of a {@link com.devonfw.module.service.common.api.Service}. */ + String KEY_SEGMENT_URL = "url"; + + /** The key segment for the port of a {@link com.devonfw.module.service.common.api.Service}. */ + String KEY_SEGMENT_PORT = "port"; + + /** The key segment for the host of a {@link com.devonfw.module.service.common.api.Service}. */ + String KEY_SEGMENT_HOST = "host"; + + /** The key segment for the protocol of a {@link com.devonfw.module.service.common.api.Service}. */ + String KEY_SEGMENT_PROTOCOL = "protocol"; + + /** The key segment for the WSDL settings of a SOAP {@link javax.jws.WebService}. */ + String KEY_SEGMENT_WSDL = "wsdl"; + + /** The key segment for the boolean property to disable download (e.g. of {@link #KEY_SEGMENT_WSDL WSDL}). */ + String KEY_SEGMENT_DISABLE_DOWNLOAD = "disable-download"; + + /** The key segment for the application specific configuration sub-tree. */ + String KEY_SEGMENT_APP = "app"; + + /** The key segment for the default configuration sub-tree. */ + String KEY_SEGMENT_DEFAULT = "default"; + + /** The key segment for the authentication mechanism (values are "basic", "oauth", etc.). */ + String KEY_SEGMENT_AUTH = "auth"; + + /** + * The key segment for the user configuration sub-tree. + * + * @see #KEY_SEGMENT_USER_LOGIN + * @see #KEY_SEGMENT_USER_PASSWORD + */ + String KEY_SEGMENT_USER = "user"; + + /** The key segment for the {@link #KEY_SEGMENT_USER user} login name. */ + String KEY_SEGMENT_USER_LOGIN = "login"; + + /** The key segment for the {@link #KEY_SEGMENT_USER user} password. */ + String KEY_SEGMENT_USER_PASSWORD = "password"; + + /** + * The key segment for the timeout configuration sub-tree. + * + * @see #KEY_SEGMENT_TIMEOUT_CONNECTION + * @see #KEY_SEGMENT_TIMEOUT_RESPONSE + */ + String KEY_SEGMENT_TIMEOUT = "timeout"; + + /** The key segment for the {@link #KEY_SEGMENT_TIMEOUT timeout} to establish a connection in seconds. */ + String KEY_SEGMENT_TIMEOUT_CONNECTION = "connection"; + + /** The key segment for the {@link #KEY_SEGMENT_TIMEOUT timeout} to wait for a response in seconds. */ + String KEY_SEGMENT_TIMEOUT_RESPONSE = "response"; + + /** The value of {@link #KEY_SEGMENT_AUTH authentication} for basic auth. */ + String VALUE_AUTH_BASIC = "basic"; + + /** The value of {@link #KEY_SEGMENT_AUTH authentication} for OAuth. */ + String VALUE_AUTH_OAUTH = "oauth"; + + /** + * The value of {@link #KEY_SEGMENT_AUTH authentication} for Basic, Oauth,JWT. + **/ + + String VALUE_AUTH_FORWARD = "authForward"; + + /** + * @return the root {@link ConfigProperties}-node with the configuration for services. + */ + ConfigProperties asConfig(); + + /** + * @return the client {@link ConfigProperties}-node with the configuration for + * {@link com.devonfw.module.service.common.api.client.ServiceClientFactory} and all related implementations. + */ + ConfigProperties asClientConfig(); + +} diff --git a/modules/service/src/main/java/com/devonfw/module/service/common/api/constants/ServiceConstants.java b/modules/service/src/main/java/com/devonfw/module/service/common/api/constants/ServiceConstants.java new file mode 100644 index 00000000..369ec8d4 --- /dev/null +++ b/modules/service/src/main/java/com/devonfw/module/service/common/api/constants/ServiceConstants.java @@ -0,0 +1,58 @@ +package com.devonfw.module.service.common.api.constants; + +import net.sf.mmm.util.exception.api.NlsRuntimeException; + +/** + * Constants for {@link com.devonfw.module.service.common.api.Service}s. + * + * @since 3.0.0 + */ +public class ServiceConstants { + + /** Key for {@link Throwable#getMessage() error message}. */ + public static final String KEY_MESSAGE = "message"; + + /** Key for {@link NlsRuntimeException#getUuid() error ID}. */ + public static final String KEY_UUID = "uuid"; + + /** Key for {@link NlsRuntimeException#getCode() error code}. */ + public static final String KEY_CODE = "code"; + + /** Key for (validation) error details. */ + public static final String KEY_ERRORS = "errors"; + + /** The services URL folder. */ + public static final String URL_FOLDER_SERVICES = "services"; + + /** The services URL path. */ + public static final String URL_PATH_SERVICES = "/" + URL_FOLDER_SERVICES; + + /** The rest URL folder. */ + public static final String URL_FOLDER_REST = "rest"; + + /** The web-service URL folder. */ + public static final String URL_FOLDER_WEB_SERVICE = "ws"; + + /** The rest services URL path. */ + public static final String URL_PATH_REST_SERVICES = URL_PATH_SERVICES + "/" + URL_FOLDER_REST; + + /** The web-service URL path. */ + public static final String URL_PATH_WEB_SERVICES = URL_PATH_SERVICES + "/" + URL_FOLDER_WEB_SERVICE; + + /** + * The variable that resolves to the {@link com.devonfw.module.basic.common.api.reflect.Devon4jPackage#getApplication() + * technical name of the application}. + */ + public static final String VARIABLE_APP = "${app}"; + + /** + * The variable that resolves to the {@link com.devonfw.module.basic.common.api.reflect.Devon4jPackage#getApplication() + * technical name of the application}. + */ + public static final String VARIABLE_LOCAL_SERVER_PORT = "${local.server.port}"; + + /** + * The variable that resolves to type of the service (e.g. "rest" for REST service and "ws" for SOAP service). + */ + public static final String VARIABLE_TYPE = "${type}"; +} diff --git a/modules/service/src/main/java/com/devonfw/module/service/common/api/header/ServiceHeaderContext.java b/modules/service/src/main/java/com/devonfw/module/service/common/api/header/ServiceHeaderContext.java new file mode 100644 index 00000000..24c48c4a --- /dev/null +++ b/modules/service/src/main/java/com/devonfw/module/service/common/api/header/ServiceHeaderContext.java @@ -0,0 +1,24 @@ +package com.devonfw.module.service.common.api.header; + +import com.devonfw.module.service.common.api.client.context.ServiceContext; + +/** + * Extends {@link ServiceContext} and allows to {@link #setHeader(String, String) set headers} to the underlying network + * protocol. + * + * @param the generic type of the {@link #getApi() service API}. + * + * @since 3.0.0 + */ +public interface ServiceHeaderContext extends ServiceContext { + + /** + * Adds a header to underlying network invocations (e.g. HTTP) triggered by a + * {@link com.devonfw.module.service.common.api.client.ServiceClientFactory#create(Class) service client}. + * + * @param key the name of the header to set. + * @param value the value of the header to set. + */ + void setHeader(String key, String value); + +} diff --git a/modules/service/src/main/java/com/devonfw/module/service/common/api/header/ServiceHeaderCustomizer.java b/modules/service/src/main/java/com/devonfw/module/service/common/api/header/ServiceHeaderCustomizer.java new file mode 100644 index 00000000..f3934358 --- /dev/null +++ b/modules/service/src/main/java/com/devonfw/module/service/common/api/header/ServiceHeaderCustomizer.java @@ -0,0 +1,21 @@ +package com.devonfw.module.service.common.api.header; + +/** + * This interface may be implemented to {@link #addHeaders(ServiceHeaderContext) customize the headers}. When a + * {@link com.devonfw.module.service.common.api.client.ServiceClientFactory#create(Class) service client} is invoked these headers + * are applied to the underlying protocol when a remote invocation is triggered via the network. Multiple + * implementations of this interface may exist as spring beans for different aspects (e.g. passing a JWT/OAuth security + * token for authentication, passing {@link com.devonfw.module.logging.common.api.LoggingConstants#CORRELATION_ID + * correlation ID}, etc.). + * + * @since 3.0.0 + */ +public interface ServiceHeaderCustomizer { + + /** + * @param context the {@link ServiceHeaderContext} that may be used to + * {@link ServiceHeaderContext#setHeader(String, String) tweak headers}. + */ + void addHeaders(ServiceHeaderContext context); + +} diff --git a/modules/service/src/main/java/com/devonfw/module/service/common/api/sync/SyncServiceClientFactory.java b/modules/service/src/main/java/com/devonfw/module/service/common/api/sync/SyncServiceClientFactory.java new file mode 100644 index 00000000..27938191 --- /dev/null +++ b/modules/service/src/main/java/com/devonfw/module/service/common/api/sync/SyncServiceClientFactory.java @@ -0,0 +1,27 @@ +package com.devonfw.module.service.common.api.sync; + +import com.devonfw.module.service.common.api.Service; +import com.devonfw.module.service.common.api.client.context.ServiceContext; + +/** + * The interface for a partial implementation of {@link com.devonfw.module.service.common.api.client.ServiceClientFactory} used to + * {@link #create(ServiceContext) create} client stubs for a {@link Service}. + * + * @see com.devonfw.module.service.common.api.client.ServiceClientFactory + * + * @since 3.0.0 + */ +public interface SyncServiceClientFactory { + + /** + * @see com.devonfw.module.service.common.api.client.ServiceClientFactory#create(Class) + * + * @param the generic type of the {@code serviceInterface}. For flexibility and being not invasive this generic is + * not bound to {@link Service} ({@code S extends Service}). + * @param context the {@link ServiceContext}. + * @return a new instance of the given {@code serviceInterface} that is a client stub. May be {@code null} if this + * implementation does not handle services for the given {@link ServiceContext}. + */ + S create(ServiceContext context); + +} diff --git a/modules/service/src/main/java/com/devonfw/module/service/common/base/config/ServiceConfigProperties.java b/modules/service/src/main/java/com/devonfw/module/service/common/base/config/ServiceConfigProperties.java new file mode 100644 index 00000000..081b341c --- /dev/null +++ b/modules/service/src/main/java/com/devonfw/module/service/common/base/config/ServiceConfigProperties.java @@ -0,0 +1,73 @@ +package com.devonfw.module.service.common.base.config; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import com.devonfw.module.basic.common.api.config.ConfigProperties; +import com.devonfw.module.basic.common.api.config.SimpleConfigProperties; +import com.devonfw.module.service.common.api.config.ServiceConfig; + +/** + * Implementation of {@link ServiceConfig} as spring-boot {@link ConfigurationProperties}.
+ * Assuming you would have the following in your {@code application.properties}: + * + *
+ * service.client.app.foo.url=https://foo.company.com/services/rest
+ * service.client.app.bar.url=https://bar.company.com/services/rest
+ * service.client.default.url=https://api.company.com/services/rest
+ * 
+ * + * Then {@link #asClientConfig()}.{@link ConfigProperties#getChildValue(String) + * getValue}({@link ServiceConfig#KEY_SEGMENT_APP}, "foo", {@link ServiceConfig#KEY_SEGMENT_URL}) would return + * "https://foo.company.com/services/rest". This URL would be used by + * {@link com.devonfw.module.service.common.impl.discovery.ServiceDiscovererImplConfig} for a + * {@link com.devonfw.module.service.common.api.Service} of the + * {@link com.devonfw.module.basic.common.api.reflect.Devon4jPackage#getApplication() application} "foo". For a + * {@link com.devonfw.module.service.common.api.Service} of the + * {@link com.devonfw.module.basic.common.api.reflect.Devon4jPackage#getApplication() application} "some" the URL + * "https://api.company.com/services/rest" would be used as default because no property "service.client.app.some.url" is + * defined. + * + * @since 3.0.0 + */ +@ConfigurationProperties(prefix = "") +public class ServiceConfigProperties implements ServiceConfig { + + private final Map service; + + private ConfigProperties configNode; + + /** + * The constructor. + */ + public ServiceConfigProperties() { + super(); + this.service = new HashMap<>(); + } + + /** + * @return client configuration {@link Map} from spring-boot {@code application.properties}. Do not modify. + */ + public Map getService() { + + return this.service; + } + + @Override + public ConfigProperties asConfig() { + + if (this.configNode == null) { + this.configNode = SimpleConfigProperties.ofFlatMap("service", this.service); + } + return this.configNode; + } + + @Override + public ConfigProperties asClientConfig() { + + return asConfig().getChild(KEY_SEGMENT_CLIENT); + } + +} diff --git a/modules/service/src/main/java/com/devonfw/module/service/common/base/context/AbstractServiceContext.java b/modules/service/src/main/java/com/devonfw/module/service/common/base/context/AbstractServiceContext.java new file mode 100644 index 00000000..3729cbe6 --- /dev/null +++ b/modules/service/src/main/java/com/devonfw/module/service/common/base/context/AbstractServiceContext.java @@ -0,0 +1,32 @@ +package com.devonfw.module.service.common.base.context; + +import com.devonfw.module.service.common.api.client.context.ServiceContext; + +/** + * The abstract base implementation of {@link ServiceContext}. + * + * @param the generic type of the {@link #getApi() service API}. + * + * @since 3.0.0 + */ +public abstract class AbstractServiceContext implements ServiceContext { + + private final Class api; + + /** + * The constructor. + * + * @param api the {@link #getApi() API}. + */ + public AbstractServiceContext(Class api) { + super(); + this.api = api; + } + + @Override + public Class getApi() { + + return this.api; + } + +} diff --git a/modules/service/src/main/java/com/devonfw/module/service/common/base/context/ServiceContextImpl.java b/modules/service/src/main/java/com/devonfw/module/service/common/base/context/ServiceContextImpl.java new file mode 100644 index 00000000..d61cfb99 --- /dev/null +++ b/modules/service/src/main/java/com/devonfw/module/service/common/base/context/ServiceContextImpl.java @@ -0,0 +1,88 @@ +package com.devonfw.module.service.common.base.context; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import com.devonfw.module.basic.common.api.config.ConfigProperties; +import com.devonfw.module.service.common.api.client.context.ServiceContext; +import com.devonfw.module.service.common.api.client.discovery.ServiceDiscoveryContext; +import com.devonfw.module.service.common.api.config.ServiceConfig; +import com.devonfw.module.service.common.api.header.ServiceHeaderContext; + +/** + * Implementation of {@link ServiceContext}. + * + * @param the generic type of the {@link #getApi() service API}. + * + * @since 3.0.0 + */ +public class ServiceContextImpl extends AbstractServiceContext + implements ServiceHeaderContext, ServiceDiscoveryContext { + + private final Map headers; + + private final Collection headerNames; + + private ConfigProperties configProperties; + + /** + * The constructor. + * + * @param api the {@link #getApi() API}. + * @param configProperties the initial {@link ConfigProperties}. May be {@link ConfigProperties#isEmpty() empty}. + */ + public ServiceContextImpl(Class api, ConfigProperties configProperties) { + super(api); + this.headers = new HashMap<>(); + this.headerNames = Collections.unmodifiableSet(this.headers.keySet()); + this.configProperties = configProperties; + } + + @Override + public String getUrl() { + + return this.configProperties.getChildValue(ServiceConfig.KEY_SEGMENT_URL); + } + + @Override + public Collection getHeaderNames() { + + return this.headerNames; + } + + @Override + public String getHeader(String name) { + + return this.headers.get(name); + } + + @Override + public void setHeader(String key, String value) { + + this.headers.put(key, value); + } + + @Override + public ConfigProperties getConfig() { + + return this.configProperties; + } + + @Override + public void setConfig(ConfigProperties configProperties) { + + String url = getUrl(); + if (url != null) { + throw new IllegalStateException( + "Discovery for " + getApi() + " is invalid as it has already been discovered (" + url + ")."); + } + String newUrl = configProperties.getChildValue(ServiceConfig.KEY_SEGMENT_URL); + if (newUrl == null) { + throw new IllegalStateException("Discovery for " + getApi() + " is invalid as no URL has been discovered."); + } + this.configProperties = configProperties; + } + +} diff --git a/modules/service/src/main/java/com/devonfw/module/service/common/impl/ServiceClientFactoryImpl.java b/modules/service/src/main/java/com/devonfw/module/service/common/impl/ServiceClientFactoryImpl.java new file mode 100644 index 00000000..28ea46b2 --- /dev/null +++ b/modules/service/src/main/java/com/devonfw/module/service/common/impl/ServiceClientFactoryImpl.java @@ -0,0 +1,125 @@ +package com.devonfw.module.service.common.impl; + +import java.util.Collection; +import java.util.Map; + +import javax.inject.Inject; + +import com.devonfw.module.basic.common.api.config.ConfigProperties; +import com.devonfw.module.basic.common.api.config.SimpleConfigProperties; +import com.devonfw.module.service.common.api.client.ServiceClientFactory; +import com.devonfw.module.service.common.api.client.discovery.ServiceDiscoverer; +import com.devonfw.module.service.common.api.header.ServiceHeaderCustomizer; +import com.devonfw.module.service.common.api.sync.SyncServiceClientFactory; +import com.devonfw.module.service.common.base.context.ServiceContextImpl; + +/** + * This is the implementation of {@link ServiceClientFactory}. + * + * @since 3.0.0 + */ +public class ServiceClientFactoryImpl implements ServiceClientFactory { + + private Collection syncServiceClientFactories; + + private Collection serviceDiscoverers; + + private Collection serviceHeaderCustomizers; + + /** + * The constructor. + */ + public ServiceClientFactoryImpl() { + super(); + } + + /** + * @param syncServiceClientFactories the {@link Collection} of {@link SyncServiceClientFactory factories} to + * {@link Inject}. + */ + @Inject + public void setSyncServiceClientFactories(Collection syncServiceClientFactories) { + + this.syncServiceClientFactories = syncServiceClientFactories; + } + + /** + * @param serviceDiscoverers the {@link Collection} of {@link ServiceDiscoverer}s to {@link Inject}. + */ + @Inject + public void setServiceDiscoverers(Collection serviceDiscoverers) { + + this.serviceDiscoverers = serviceDiscoverers; + } + + /** + * @param serviceHeaderCustomizers the {@link Collection} of {@link ServiceHeaderCustomizer}s to {@link Inject}. + */ + @Inject + public void setServiceHeaderCustomizers(Collection serviceHeaderCustomizers) { + + this.serviceHeaderCustomizers = serviceHeaderCustomizers; + } + + @Override + public S create(Class serviceInterface) { + + return create(serviceInterface, null); + } + + @Override + public S create(Class serviceInterface, Map properties) { + + ConfigProperties configPropreties = ConfigProperties.EMPTY; + if ((properties != null) && !properties.isEmpty()) { + configPropreties = SimpleConfigProperties.ofFlatMap(properties); + } + ServiceContextImpl context = new ServiceContextImpl<>(serviceInterface, configPropreties); + discovery(context); + customizeHeaders(context); + S serviceClient = createClient(serviceInterface, context); + return serviceClient; + } + + private S createClient(Class serviceInterface, ServiceContextImpl context) { + + S serviceClient = null; + for (SyncServiceClientFactory factory : this.syncServiceClientFactories) { + serviceClient = factory.create(context); + if (serviceClient != null) { + break; + } + } + if (serviceClient == null) { + throw new IllegalStateException( + "Unsuppoerted service type - client could not be created by any factory for " + serviceInterface); + } + return serviceClient; + } + + private void customizeHeaders(ServiceContextImpl context) { + + for (ServiceHeaderCustomizer headerCustomizer : this.serviceHeaderCustomizers) { + headerCustomizer.addHeaders(context); + } + } + + private void discovery(ServiceContextImpl context) { + + if (context.getUrl() != null) { + return; + } + if (this.serviceDiscoverers != null) { + for (ServiceDiscoverer discoverer : this.serviceDiscoverers) { + discoverer.discover(context); + if (context.getUrl() != null) { + break; + } + } + } + if (context.getUrl() == null) { + throw new IllegalStateException("Service discovery failed for " + context.getApi()); + } + } + +} diff --git a/modules/service/src/main/java/com/devonfw/module/service/common/impl/discovery/ServiceDiscovererImplConfig.java b/modules/service/src/main/java/com/devonfw/module/service/common/impl/discovery/ServiceDiscovererImplConfig.java new file mode 100644 index 00000000..93039f3f --- /dev/null +++ b/modules/service/src/main/java/com/devonfw/module/service/common/impl/discovery/ServiceDiscovererImplConfig.java @@ -0,0 +1,131 @@ +package com.devonfw.module.service.common.impl.discovery; + +import javax.inject.Inject; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.context.WebServerInitializedEvent; +import org.springframework.context.ApplicationListener; + +import com.devonfw.module.basic.common.api.config.ConfigProperties; +import com.devonfw.module.basic.common.api.config.MutableConfigProperties; +import com.devonfw.module.basic.common.api.reflect.Devon4jPackage; +import com.devonfw.module.service.common.api.client.discovery.ServiceDiscoverer; +import com.devonfw.module.service.common.api.client.discovery.ServiceDiscoveryContext; +import com.devonfw.module.service.common.api.config.ServiceConfig; +import com.devonfw.module.service.common.api.constants.ServiceConstants; + +/** + * A simple implementation of {@link ServiceDiscoverer} that is using {@link ServiceConfig}. The Service URL can be + * configured per application providing the service as well as statically for all services (e.g. in case of a central + * API gateway or a local proxy like Envoy + * Sidecar Proxy). + * + * @see com.devonfw.module.service.common.base.config.ServiceConfigProperties + * + * @since 3.0.0 + */ +public class ServiceDiscovererImplConfig implements ServiceDiscoverer, ApplicationListener { + + // @Value("${local.server.port}") + private int localServerPort; + + @Value("${server.context-path:}") + private String contextPath; + + private ServiceConfig config; + + /** + * @param config the {@link ServiceConfig} to {@link Inject}. + */ + @Inject + public void setConfig(ServiceConfig config) { + + this.config = config; + } + + @Override + public void onApplicationEvent(WebServerInitializedEvent event) { + + this.localServerPort = event.getWebServer().getPort(); + } + + @Override + public void discover(ServiceDiscoveryContext context) { + + Class api = context.getApi(); + Devon4jPackage devon4jPackage = Devon4jPackage.of(api); + String application = devon4jPackage.getApplication(); + if (application == null) { + application = api.getName(); + } + ConfigProperties clientNode = this.config.asClientConfig(); + ConfigProperties appNode = clientNode.getChild(ServiceConfig.KEY_SEGMENT_APP, application); + ConfigProperties defaultNode = clientNode.getChild(ServiceConfig.KEY_SEGMENT_DEFAULT); + MutableConfigProperties configNode = appNode.inherit(defaultNode); + configNode = context.getConfig().inherit(configNode); + String url = configNode.getChildValue(ServiceConfig.KEY_SEGMENT_URL); + if (url == null) { + String host = configNode.getChildValue(ServiceConfig.KEY_SEGMENT_HOST); + if (host == null) { + return; + } + String port = clientNode.getChild(ServiceConfig.KEY_SEGMENT_PORT).getValue(); + String protocol = clientNode.getChild(ServiceConfig.KEY_SEGMENT_PROTOCOL).getValue(); + if (protocol == null) { + if ("443".equals(port)) { + protocol = "https"; + } + protocol = "http"; + } + if ((port == null) && (isLocalhost(host))) { + port = Integer.toString(this.localServerPort); + } + StringBuilder buffer = new StringBuilder(); + buffer.append(protocol); + buffer.append("://"); + buffer.append(host); + if (port != null) { + buffer.append(':'); + buffer.append(port); + } + if (!this.contextPath.isEmpty()) { + buffer.append(this.contextPath); + buffer.append('/'); + } + buffer.append(ServiceConstants.URL_PATH_SERVICES); + buffer.append('/'); + buffer.append(ServiceConstants.VARIABLE_TYPE); + url = buffer.toString(); + } + url = resolveVariables(url, application); + configNode.setChildValue(ServiceConfig.KEY_SEGMENT_URL, url); + context.setConfig(configNode); + } + + private String resolveVariables(String url2, String application) { + + String resolvedUrl = url2; + resolvedUrl = resolvedUrl.replace(ServiceConstants.VARIABLE_APP, application); + resolvedUrl = resolvedUrl.replace(ServiceConstants.VARIABLE_LOCAL_SERVER_PORT, + Integer.toString(this.localServerPort)); + return resolvedUrl; + } + + private boolean isLocalhost(String host) { + + if ("localhost".equalsIgnoreCase(host)) { + return true; + } + if ("127.0.0.1".equals(host)) { + return true; + } + if ("0:0:0:0:0:0:0:1".equals(host)) { + return true; + } + if ("::1".equals(host)) { + return true; + } + return false; + } + +} diff --git a/modules/service/src/main/java/com/devonfw/module/service/common/impl/header/ServiceHeaderCustomizerAuthForward.java b/modules/service/src/main/java/com/devonfw/module/service/common/impl/header/ServiceHeaderCustomizerAuthForward.java new file mode 100644 index 00000000..0d9edc5c --- /dev/null +++ b/modules/service/src/main/java/com/devonfw/module/service/common/impl/header/ServiceHeaderCustomizerAuthForward.java @@ -0,0 +1,42 @@ +package com.devonfw.module.service.common.impl.header; + +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +import com.devonfw.module.service.common.api.config.ServiceConfig; +import com.devonfw.module.service.common.api.header.ServiceHeaderContext; +import com.devonfw.module.service.common.api.header.ServiceHeaderCustomizer; + +/** + * @author ssarmoka + * + */ +public class ServiceHeaderCustomizerAuthForward implements ServiceHeaderCustomizer { + + private static final String AUTHORIZATION = "Authorization"; + + /** + * + * The constructor. + */ + public ServiceHeaderCustomizerAuthForward() { + super(); + } + + @Override + public void addHeaders(ServiceHeaderContext context) { + + String auth = context.getConfig().getChildValue(ServiceConfig.KEY_SEGMENT_AUTH); + if (!ServiceConfig.VALUE_AUTH_FORWARD.equals(auth)) { + return; + } + SecurityContext securityContext = SecurityContextHolder.getContext(); + if (securityContext == null) { + return; + } + String authorizationHeader = context.getConfig().getChildValue(AUTHORIZATION); + context.setHeader(AUTHORIZATION, authorizationHeader); + + } + +} diff --git a/modules/service/src/main/java/com/devonfw/module/service/common/impl/header/ServiceHeaderCustomizerBasicAuth.java b/modules/service/src/main/java/com/devonfw/module/service/common/impl/header/ServiceHeaderCustomizerBasicAuth.java new file mode 100644 index 00000000..d0871df2 --- /dev/null +++ b/modules/service/src/main/java/com/devonfw/module/service/common/impl/header/ServiceHeaderCustomizerBasicAuth.java @@ -0,0 +1,48 @@ +package com.devonfw.module.service.common.impl.header; + +import org.springframework.util.Base64Utils; + +import com.devonfw.module.basic.common.api.config.ConfigProperties; +import com.devonfw.module.logging.common.api.LoggingConstants; +import com.devonfw.module.service.common.api.config.ServiceConfig; +import com.devonfw.module.service.common.api.header.ServiceHeaderContext; +import com.devonfw.module.service.common.api.header.ServiceHeaderCustomizer; + +/** + * Implementation of {@link ServiceHeaderCustomizer} that passes the {@link LoggingConstants#CORRELATION_ID} to a + * subsequent {@link com.devonfw.module.service.common.api.Service} invocation. + * + * @since 3.0.0 + */ +public class ServiceHeaderCustomizerBasicAuth implements ServiceHeaderCustomizer { + + /** + * The constructor. + */ + public ServiceHeaderCustomizerBasicAuth() { + + super(); + } + + @Override + public void addHeaders(ServiceHeaderContext context) { + + String auth = context.getConfig().getChildValue(ServiceConfig.KEY_SEGMENT_AUTH); + if (!"basic".equals(auth)) { + return; + } + ConfigProperties userConfig = context.getConfig().getChild(ServiceConfig.KEY_SEGMENT_USER); + if (userConfig.isEmpty()) { + return; + } + String login = userConfig.getChildValue(ServiceConfig.KEY_SEGMENT_USER_LOGIN); + if (login == null) { + return; + } + String password = userConfig.getChild(ServiceConfig.KEY_SEGMENT_USER_PASSWORD).getValue(String.class, login); + String payload = login + ":" + password; + String authorizationHeader = "Basic " + Base64Utils.encodeToString(payload.getBytes()); + context.setHeader("Authorization", authorizationHeader); + } + +} diff --git a/modules/service/src/main/java/com/devonfw/module/service/common/impl/header/ServiceHeaderCustomizerCorrelationId.java b/modules/service/src/main/java/com/devonfw/module/service/common/impl/header/ServiceHeaderCustomizerCorrelationId.java new file mode 100644 index 00000000..340e0ebb --- /dev/null +++ b/modules/service/src/main/java/com/devonfw/module/service/common/impl/header/ServiceHeaderCustomizerCorrelationId.java @@ -0,0 +1,48 @@ +package com.devonfw.module.service.common.impl.header; + +import org.slf4j.MDC; + +import com.devonfw.module.logging.common.api.LoggingConstants; +import com.devonfw.module.logging.common.impl.DiagnosticContextFilter; +import com.devonfw.module.service.common.api.header.ServiceHeaderContext; +import com.devonfw.module.service.common.api.header.ServiceHeaderCustomizer; + +/** + * Implementation of {@link ServiceHeaderCustomizer} that passes the {@link LoggingConstants#CORRELATION_ID} to a + * subsequent {@link com.devonfw.module.service.common.api.Service} invocation. + * + * @since 3.0.0 + */ +public class ServiceHeaderCustomizerCorrelationId implements ServiceHeaderCustomizer { + + private final String headerName; + + /** + * The constructor. + */ + public ServiceHeaderCustomizerCorrelationId() { + + this(DiagnosticContextFilter.CORRELATION_ID_HEADER_NAME_DEFAULT); + } + + /** + * The constructor. + * + * @param headerName the name of the header for the correlation ID. + */ + public ServiceHeaderCustomizerCorrelationId(String headerName) { + + super(); + this.headerName = headerName; + } + + @Override + public void addHeaders(ServiceHeaderContext context) { + + String correlationId = MDC.get(LoggingConstants.CORRELATION_ID); + if ((correlationId != null) && (!correlationId.isEmpty())) { + context.setHeader(this.headerName, correlationId); + } + } + +} diff --git a/modules/service/src/main/java/com/devonfw/module/service/common/impl/header/ServiceHeaderCustomizerOAuth.java b/modules/service/src/main/java/com/devonfw/module/service/common/impl/header/ServiceHeaderCustomizerOAuth.java new file mode 100644 index 00000000..28fcc1f2 --- /dev/null +++ b/modules/service/src/main/java/com/devonfw/module/service/common/impl/header/ServiceHeaderCustomizerOAuth.java @@ -0,0 +1,58 @@ +package com.devonfw.module.service.common.impl.header; + +import java.util.Map; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +import com.devonfw.module.logging.common.api.LoggingConstants; +import com.devonfw.module.service.common.api.config.ServiceConfig; +import com.devonfw.module.service.common.api.header.ServiceHeaderContext; +import com.devonfw.module.service.common.api.header.ServiceHeaderCustomizer; + +/** + * Implementation of {@link ServiceHeaderCustomizer} that passes the {@link LoggingConstants#CORRELATION_ID} to a + * subsequent {@link com.devonfw.module.service.common.api.Service} invocation. + * + * @since 3.0.0 + */ +public class ServiceHeaderCustomizerOAuth implements ServiceHeaderCustomizer { + + /** + * The constructor. + */ + public ServiceHeaderCustomizerOAuth() { + + super(); + } + + @Override + public void addHeaders(ServiceHeaderContext context) { + + String auth = context.getConfig().getChildValue(ServiceConfig.KEY_SEGMENT_AUTH); + if (!"oauth".equals(auth)) { + return; + } + SecurityContext securityContext = SecurityContextHolder.getContext(); + if (securityContext == null) { + return; + } + Authentication authentication = securityContext.getAuthentication(); + if (authentication == null) { + return; + } + Object details = authentication.getDetails(); + if (!(details instanceof Map)) { + return; + } + Map map = (Map) details; + Object oauthToken = map.get("oauth.token"); + if (oauthToken == null) { + return; + } + String authorizationHeader = "Bearer " + oauthToken; + context.setHeader("Authorization", authorizationHeader); + } + +} diff --git a/modules/test-jpa/pom.xml b/modules/test-jpa/pom.xml new file mode 100644 index 00000000..eb292fa0 --- /dev/null +++ b/modules/test-jpa/pom.xml @@ -0,0 +1,29 @@ + + 4.0.0 + + com.devonfw.java.dev + devon4j-modules + dev-SNAPSHOT + + com.devonfw.java.modules + devon4j-test-jpa + ${devon4j.version} + ${project.artifactId} + Module with code and configuration for JPA/DB tests of the Open Application Standard Platform for Java (devon4j). + + + + com.devonfw.java.modules + devon4j-test + + + com.devonfw.java.modules + devon4j-jpa + + + org.flywaydb + flyway-core + + + \ No newline at end of file diff --git a/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/ComponentDbTest.java b/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/ComponentDbTest.java new file mode 100644 index 00000000..4494916c --- /dev/null +++ b/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/ComponentDbTest.java @@ -0,0 +1,77 @@ +package com.devonfw.module.test.common.base; + +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; + +import com.devonfw.module.test.common.api.category.CategoryComponentTest; +import com.devonfw.module.test.common.base.ComponentTest; +import com.devonfw.module.test.common.base.clean.TestCleaner; + +/** + * Combination of {@link DbTest} with {@link ComponentTest}. + */ +@RunWith(SpringRunner.class) +@TestExecutionListeners({ TransactionalTestExecutionListener.class, DependencyInjectionTestExecutionListener.class }) +@Category(CategoryComponentTest.class) +public abstract class ComponentDbTest extends DbTest { + + @PersistenceContext + private EntityManager entityManager; + + @Inject + private TestCleaner testUtility; + + @Override + protected void doSetUp() { + + super.doSetUp(); + if (isInitialSetup()) { + JpaTestInitializer.setJpaEntityManager(this.entityManager); + } + if (isPerformCleanup()) { + if (isAllowMultiCleanup() || isInitialSetup()) { + this.testUtility.cleanup(); + } + } + } + + /** + * @return {@code true} if {@link TestCleaner#cleanup()} should be enabled, {@code false} otherwise. Override to + * change behavior for your test. + */ + @Override + protected boolean isPerformCleanup() { + + return true; + } + + /** + * @return {@code true} to allow that {@link TestCleaner#cleanup()} is invoked for each test-method of your test + * (potentially multiple times), {@code false} to ensure that {@link TestCleaner#cleanup()} is never invoked + * multiple times per test class (e.g. to speed up read-only tests that will never have side-effects). Will be + * ignored if {@link #isPerformCleanup()} returns {@code false}. Override to change behavior for your test. + */ + @Override + protected boolean isAllowMultiCleanup() { + + return true; + } + + /** + * @return the instance of {@link TestCleaner}. + */ + @Override + public TestCleaner getTestUtility() { + + return this.testUtility; + } + +} diff --git a/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/DbTest.java b/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/DbTest.java new file mode 100644 index 00000000..a71dd931 --- /dev/null +++ b/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/DbTest.java @@ -0,0 +1,69 @@ +package com.devonfw.module.test.common.base; + +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import com.devonfw.module.test.common.base.BaseTest; +import com.devonfw.module.test.common.base.clean.TestCleaner; + +/** + * Extends {@link BaseTest} with the following features: + *
    + *
  • Automatically register the {@link EntityManager} of the current spring-test. See {@link JpaTestInitializer} for + * further details.
  • + *
  • Automatically performs {@link TestCleaner#cleanup() cleanup} to prevent side-effects. Can be configured by + * overriding {@link #isPerformCleanup()} and {@link #isAllowMultiCleanup()}.
  • + *
+ */ +public abstract class DbTest extends BaseTest { + + @PersistenceContext + private EntityManager entityManager; + + @Inject + private TestCleaner testUtility; + + @Override + protected void doSetUp() { + + super.doSetUp(); + if (isInitialSetup()) { + JpaTestInitializer.setJpaEntityManager(this.entityManager); + } + if (isPerformCleanup()) { + if (isAllowMultiCleanup() || isInitialSetup()) { + this.testUtility.cleanup(); + } + } + } + + /** + * @return {@code true} if {@link TestCleaner#cleanup()} should be enabled, {@code false} otherwise. Override to + * change behavior for your test. + */ + protected boolean isPerformCleanup() { + + return true; + } + + /** + * @return {@code true} to allow that {@link TestCleaner#cleanup()} is invoked for each test-method of your test + * (potentially multiple times), {@code false} to ensure that {@link TestCleaner#cleanup()} is never invoked + * multiple times per test class (e.g. to speed up read-only tests that will never have side-effects). Will be + * ignored if {@link #isPerformCleanup()} returns {@code false}. Override to change behavior for your test. + */ + protected boolean isAllowMultiCleanup() { + + return true; + } + + /** + * @return the instance of {@link TestCleaner}. + */ + public TestCleaner getTestUtility() { + + return this.testUtility; + } + +} diff --git a/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/JpaTestInitializer.java b/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/JpaTestInitializer.java new file mode 100644 index 00000000..860c6e5a --- /dev/null +++ b/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/JpaTestInitializer.java @@ -0,0 +1,37 @@ +package com.devonfw.module.test.common.base; + +import javax.persistence.EntityManager; + +import com.devonfw.module.jpa.dataaccess.api.JpaInitializer; + +/** + * Helper class giving access to {@link #setJpaEntityManager(EntityManager) set} the {@link EntityManager} for + * tests.
+ * The {@code spring-test} infrastructure is very powerful and does a lot of magic for you. However, in some edge cases + * you need to understand what is going on behind the scenes to workaround some problems. On case is the regular + * {@link com.devonfw.module.jpa.dataaccess.api.JpaInitializer} that initializes the {@link EntityManager} during the + * bootstrapping of the spring context and makes it internally available to static methods (e.g. + * {@link com.devonfw.module.jpa.dataaccess.api.JpaHelper#asEntity(com.devonfw.module.basic.common.api.reference.Ref, Class)}). + * However, {@code spring-test} internally reuses the spring context to boost performance if multiple spring tests are + * run using the same context. However, if then another spring test runs with a different context then that spring + * context will be setup overwriting the static instance of {@link EntityManager}. Still everything works as expected. + * But now if another spring-test is executed using a previous configuration that previous spring context will be + * magically reused by {@code spring-test}. In such case the static instance of {@link EntityManager} has to be set back + * to the {@link EntityManager} of the current spring context otherwise you will get strange errors in your tests. In + * order to archive this goal you need to inject the {@link EntityManager} into each of your spring tests and pass it + * into the {@link #setJpaEntityManager(EntityManager) method} offered here. To simplify your life you can simply derive + * from {@link SubsystemDbTest}, + */ +public class JpaTestInitializer extends JpaInitializer { + + private static final JpaTestInitializer INSTANCE = new JpaTestInitializer(); + + /** + * @param entityManager the {@link EntityManager} to set. + */ + public static final void setJpaEntityManager(EntityManager entityManager) { + + INSTANCE.setEntityManager(entityManager, false); + } + +} diff --git a/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/SubsystemDbTest.java b/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/SubsystemDbTest.java new file mode 100644 index 00000000..4664e9d8 --- /dev/null +++ b/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/SubsystemDbTest.java @@ -0,0 +1,21 @@ +package com.devonfw.module.test.common.base; + +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; + +import com.devonfw.module.test.common.api.category.CategorySubsystemTest; +import com.devonfw.module.test.common.base.SubsystemTest; + +/** + * Combination of {@link DbTest} with {@link SubsystemTest}. + */ +@RunWith(SpringJUnit4ClassRunner.class) +@TestExecutionListeners({ TransactionalTestExecutionListener.class, DependencyInjectionTestExecutionListener.class }) +@Category(CategorySubsystemTest.class) +public abstract class SubsystemDbTest extends DbTest { + +} diff --git a/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/SystemDbTest.java b/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/SystemDbTest.java new file mode 100644 index 00000000..86a89bc6 --- /dev/null +++ b/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/SystemDbTest.java @@ -0,0 +1,14 @@ +package com.devonfw.module.test.common.base; + +import org.junit.experimental.categories.Category; + +import com.devonfw.module.test.common.api.category.CategorySystemTest; +import com.devonfw.module.test.common.base.SystemTest; + +/** + * Combination of {@link DbTest} with {@link SystemTest}. + */ +@Category(CategorySystemTest.class) +public class SystemDbTest extends DbTest { + +} diff --git a/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/clean/AbstractTestCleaner.java b/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/clean/AbstractTestCleaner.java new file mode 100644 index 00000000..4e2eba8d --- /dev/null +++ b/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/clean/AbstractTestCleaner.java @@ -0,0 +1,14 @@ +package com.devonfw.module.test.common.base.clean; + +/** + * Interface providing ability to {@link #cleanup() cleanup}. + */ +public abstract interface AbstractTestCleaner { + + /** + * Performs a cleanup of contextual data. E.g. it may rest the database so according side-effects from previous tests + * are eliminated. + */ + void cleanup(); + +} diff --git a/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/clean/TestCleaner.java b/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/clean/TestCleaner.java new file mode 100644 index 00000000..60a28f1a --- /dev/null +++ b/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/clean/TestCleaner.java @@ -0,0 +1,8 @@ +package com.devonfw.module.test.common.base.clean; + +/** + * Interface for the central component performing all {@link #cleanup() cleanups}. + */ +public interface TestCleaner extends AbstractTestCleaner { + +} diff --git a/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/clean/TestCleanerImpl.java b/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/clean/TestCleanerImpl.java new file mode 100644 index 00000000..d36717ae --- /dev/null +++ b/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/clean/TestCleanerImpl.java @@ -0,0 +1,31 @@ +package com.devonfw.module.test.common.base.clean; + +import java.util.List; + +import javax.inject.Inject; + +/** + * Implementation of {@link TestCleaner}. Simply executes all {@link TestCleanerPlugin}s on {@link #cleanup()}. + */ +public class TestCleanerImpl implements TestCleaner { + + private List plugins; + + /** + * @param plugins the {@link List} of {@link TestCleanerPlugin}s to {@link Inject}. + */ + @Inject + public void setPlugins(List plugins) { + + this.plugins = plugins; + } + + @Override + public void cleanup() { + + for (TestCleanerPlugin plugin : this.plugins) { + plugin.cleanup(); + } + } + +} diff --git a/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/clean/TestCleanerPlugin.java b/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/clean/TestCleanerPlugin.java new file mode 100644 index 00000000..a023ead6 --- /dev/null +++ b/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/clean/TestCleanerPlugin.java @@ -0,0 +1,8 @@ +package com.devonfw.module.test.common.base.clean; + +/** + * Interface for a single "plugin" of {@link TestCleanerImpl} performing all {@link #cleanup() cleanups}. + */ +public interface TestCleanerPlugin extends AbstractTestCleaner { + +} diff --git a/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/clean/TestCleanerPluginFlyway.java b/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/clean/TestCleanerPluginFlyway.java new file mode 100644 index 00000000..909fb263 --- /dev/null +++ b/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/clean/TestCleanerPluginFlyway.java @@ -0,0 +1,43 @@ +package com.devonfw.module.test.common.base.clean; + +import javax.inject.Inject; + +import org.flywaydb.core.Flyway; + +/** + * Implementation of {@link TestCleanerPlugin} base on {@link Flyway}. It will {@link Flyway#clean() clean} and + * {@link Flyway#migrate() migrate} on {@link #cleanup()}. Therefore after {@link #cleanup()} only DDL and master-data + * will be left in the database. + */ +public class TestCleanerPluginFlyway implements TestCleanerPlugin { + + @Inject + private Flyway flyway; + + /** + * The constructor. + */ + public TestCleanerPluginFlyway() { + + super(); + } + + /** + * The constructor. + * + * @param flyway the {@link Flyway} instance. + */ + public TestCleanerPluginFlyway(Flyway flyway) { + + super(); + this.flyway = flyway; + } + + @Override + public void cleanup() { + + this.flyway.clean(); + this.flyway.migrate(); + } + +} diff --git a/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/clean/TestCleanerPluginNone.java b/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/clean/TestCleanerPluginNone.java new file mode 100644 index 00000000..1df70541 --- /dev/null +++ b/modules/test-jpa/src/main/java/com/devonfw/module/test/common/base/clean/TestCleanerPluginNone.java @@ -0,0 +1,16 @@ +package com.devonfw.module.test.common.base.clean; + +/** + * Implementation of {@link TestCleanerPlugin} that simply does nothing. May be used to satisfy injections as spring + * would throw an exception if no {@link TestCleanerPlugin} is available at all for injection into + * {@link TestCleanerImpl}. + */ +public class TestCleanerPluginNone implements TestCleanerPlugin { + + @Override + public void cleanup() { + + // do nothing by design + } + +} diff --git a/modules/test/pom.xml b/modules/test/pom.xml new file mode 100644 index 00000000..e41366ea --- /dev/null +++ b/modules/test/pom.xml @@ -0,0 +1,30 @@ + + 4.0.0 + + com.devonfw.java.dev + devon4j-modules + dev-SNAPSHOT + + com.devonfw.java.modules + devon4j-test + ${devon4j.version} + ${project.artifactId} + Module with code and configuration for tests of the Open Application Standard Platform for Java (devon4j). + + + + junit + junit + + + + org.springframework.boot + spring-boot-starter-test + + + org.mockito + mockito-core + + + \ No newline at end of file diff --git a/modules/test/src/main/java/com/devonfw/module/test/common/api/category/CategoryComponentTest.java b/modules/test/src/main/java/com/devonfw/module/test/common/api/category/CategoryComponentTest.java new file mode 100644 index 00000000..00cee46d --- /dev/null +++ b/modules/test/src/main/java/com/devonfw/module/test/common/api/category/CategoryComponentTest.java @@ -0,0 +1,10 @@ +package com.devonfw.module.test.common.api.category; + +/** + * This is the JUnit {@link org.junit.experimental.categories.Category} for a component test. + * + */ +public interface CategoryComponentTest { + +} diff --git a/modules/test/src/main/java/com/devonfw/module/test/common/api/category/CategoryModuleTest.java b/modules/test/src/main/java/com/devonfw/module/test/common/api/category/CategoryModuleTest.java new file mode 100644 index 00000000..c147f4a3 --- /dev/null +++ b/modules/test/src/main/java/com/devonfw/module/test/common/api/category/CategoryModuleTest.java @@ -0,0 +1,10 @@ +package com.devonfw.module.test.common.api.category; + +/** + * This is the JUnit {@link org.junit.experimental.categories.Category} for a module test. + * + */ +public interface CategoryModuleTest { + +} diff --git a/modules/test/src/main/java/com/devonfw/module/test/common/api/category/CategorySubsystemTest.java b/modules/test/src/main/java/com/devonfw/module/test/common/api/category/CategorySubsystemTest.java new file mode 100644 index 00000000..8148c937 --- /dev/null +++ b/modules/test/src/main/java/com/devonfw/module/test/common/api/category/CategorySubsystemTest.java @@ -0,0 +1,10 @@ +package com.devonfw.module.test.common.api.category; + +/** + * This is the JUnit {@link org.junit.experimental.categories.Category} for an integration test. + * + */ +public interface CategorySubsystemTest { + +} diff --git a/modules/test/src/main/java/com/devonfw/module/test/common/api/category/CategorySystemTest.java b/modules/test/src/main/java/com/devonfw/module/test/common/api/category/CategorySystemTest.java new file mode 100644 index 00000000..aa8fc925 --- /dev/null +++ b/modules/test/src/main/java/com/devonfw/module/test/common/api/category/CategorySystemTest.java @@ -0,0 +1,10 @@ +package com.devonfw.module.test.common.api.category; + +/** + * This is the JUnit {@link org.junit.experimental.categories.Category} for an system test. + * + */ +public interface CategorySystemTest { + +} diff --git a/modules/test/src/main/java/com/devonfw/module/test/common/base/BaseTest.java b/modules/test/src/main/java/com/devonfw/module/test/common/base/BaseTest.java new file mode 100644 index 00000000..3f0db00d --- /dev/null +++ b/modules/test/src/main/java/com/devonfw/module/test/common/base/BaseTest.java @@ -0,0 +1,92 @@ +package com.devonfw.module.test.common.base; + +import org.assertj.core.api.Assertions; +import org.junit.After; +import org.junit.Before; + +/** + * This is the {@code abstract} base class for all tests. In most cases it will be convenient to extend this class.
+ *
+ * Although it does not contain abstract methods, the class itself is declared {@code abstract} to clarify its purpose. + *
+ *
+ * This class provides {@code final} methods {@link #setUp()} and {@link #tearDown()} which call {@code protected} + * methods {@link #doSetUp()} and {@link #doTearDown()} internally.
+ * Both methods {@link #doSetUp()} and {@link #doTearDown()} are left empty here. If some default behaviour is desired + * during test setup or teardown these methods should be overridden by the subclass.
+ * Implementations must call the super implementation. This call should always happen at the beginning of the + * implementation. This helps to avoid confusion of call orders.
+ *
+ * The following listing should clarify the intention: + * + *
+ * public class MyTest extends BaseTest {
+ *
+ *   @Override
+ *   protected void doSetUp() {
+ *
+ *     super.doSetUp();
+ *     // ... my code
+ *   }
+ * }
+ * 
+ * + * Additional value is provided by {@link #isInitialSetup()} that may be helpful for specific implementations of + * {@link #doSetUp()} where you want to do some cleanup only once for the test-class rather than for every test method. + * Unlike {@link org.junit.BeforeClass} this can be used in non-static method code so you have access to injected + * dependencies. + * + * @author shuber, jmolinar + */ +public abstract class BaseTest extends Assertions { + /** + * Indicates if the test class is to be set up for the first time. {@code true} indicates that the class has already + * been set up (e.g., database setup) for the execution of an preceding test method. + */ + protected static boolean INITIALIZED = false; + + /** + * Suggests to use {@link #doSetUp()} method before each tests. + */ + @Before + public final void setUp() { + + // Simply sets INITIALIZED to true when setUp is called for the first time. + doSetUp(); + if (!INITIALIZED) { + INITIALIZED = true; + } + } + + /** + * Suggests to use {@link #doTearDown()} method before each tests. + */ + @After + public final void tearDown() { + + doTearDown(); + } + + /** + * @return {@code true} if this JUnit class is invoked for the first time (first test method is called), {@code false} + * otherwise (if this is a subsequent invocation). + */ + protected boolean isInitialSetup() { + + return INITIALIZED; + } + + /** + * Provides initialization previous to the creation of the text fixture. + */ + protected void doSetUp() { + + } + + /** + * Provides clean up after tests. + */ + protected void doTearDown() { + + } +} \ No newline at end of file diff --git a/modules/test/src/main/java/com/devonfw/module/test/common/base/ComponentTest.java b/modules/test/src/main/java/com/devonfw/module/test/common/base/ComponentTest.java new file mode 100644 index 00000000..2b3f2934 --- /dev/null +++ b/modules/test/src/main/java/com/devonfw/module/test/common/base/ComponentTest.java @@ -0,0 +1,24 @@ +package com.devonfw.module.test.common.base; + +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; + +import com.devonfw.module.test.common.api.category.CategoryComponentTest; + +/** + * This is the abstract base class for a component test. You are free to create your component tests as you like just by + * annotating {@link CategoryComponentTest} using {@link Category}. However, in most cases it will be convenient just to + * extend this class. + * + * @see CategoryComponentTest + */ +@RunWith(SpringRunner.class) +@TestExecutionListeners({ TransactionalTestExecutionListener.class, DependencyInjectionTestExecutionListener.class }) +@Category(CategoryComponentTest.class) +public abstract class ComponentTest extends BaseTest { + +} diff --git a/modules/test/src/main/java/com/devonfw/module/test/common/base/ModuleTest.java b/modules/test/src/main/java/com/devonfw/module/test/common/base/ModuleTest.java new file mode 100644 index 00000000..98526533 --- /dev/null +++ b/modules/test/src/main/java/com/devonfw/module/test/common/base/ModuleTest.java @@ -0,0 +1,18 @@ +package com.devonfw.module.test.common.base; + +import org.junit.experimental.categories.Category; + +import com.devonfw.module.test.common.api.category.CategoryModuleTest; + +/** + * This is the abstract base class for a module test. You are free to create your module tests as you like just by + * annotating {@link CategoryModuleTest} using {@link Category}. However, in most cases it will be convenient just to + * extend this class. + * + * @see CategoryModuleTest + * + */ +@Category(CategoryModuleTest.class) +public abstract class ModuleTest extends BaseTest { + +} diff --git a/modules/test/src/main/java/com/devonfw/module/test/common/base/SubsystemTest.java b/modules/test/src/main/java/com/devonfw/module/test/common/base/SubsystemTest.java new file mode 100644 index 00000000..6d96e416 --- /dev/null +++ b/modules/test/src/main/java/com/devonfw/module/test/common/base/SubsystemTest.java @@ -0,0 +1,33 @@ +package com.devonfw.module.test.common.base; + +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; + +import com.devonfw.module.test.common.api.category.CategorySubsystemTest; + +/** + * This is the abstract base class for an integrative test of a sub-system (e.g. your application backend). You are free + * to create your integration tests as you like just by annotating {@link CategorySubsystemTest} using {@link Category}. + * However, in most cases it will be convenient just to extend this class. Also we recommend to use the {@code spring} + * framework and utilize {@code spring-boot-test}. In such case create an abstract base-class for the + * {@link SubsystemTest}s of your application as following: + * + *
+ * @{@link org.springframework.boot.test.context.SpringBootTest} (webEnvironment =
+ * {@link org.springframework.boot.test.context.SpringBootTest.WebEnvironment#RANDOM_PORT}, classes =
+ * MyApplication.class) public abstract class MyApplicationSubsystemTest {
+ * }
+ * 
+ * + * @see CategorySubsystemTest + */ +@RunWith(SpringJUnit4ClassRunner.class) +@TestExecutionListeners({ TransactionalTestExecutionListener.class, DependencyInjectionTestExecutionListener.class }) +@Category(CategorySubsystemTest.class) +public abstract class SubsystemTest extends BaseTest { + +} diff --git a/modules/test/src/main/java/com/devonfw/module/test/common/base/SystemTest.java b/modules/test/src/main/java/com/devonfw/module/test/common/base/SystemTest.java new file mode 100644 index 00000000..78d46f8a --- /dev/null +++ b/modules/test/src/main/java/com/devonfw/module/test/common/base/SystemTest.java @@ -0,0 +1,17 @@ +package com.devonfw.module.test.common.base; + +import org.junit.experimental.categories.Category; + +import com.devonfw.module.test.common.api.category.CategorySystemTest; + +/** + * This is the abstract base class for a system test. You are free to create your system tests as you like just by + * annotating {@link CategorySystemTest} using {@link Category}. However, in most cases it will be convenient just to + * extend this class. + * + * @see CategorySystemTest + */ +@Category(CategorySystemTest.class) +public abstract class SystemTest extends BaseTest { + +} diff --git a/modules/test/src/main/resources/logback.xml b/modules/test/src/main/resources/logback.xml new file mode 100644 index 00000000..5e8ff52c --- /dev/null +++ b/modules/test/src/main/resources/logback.xml @@ -0,0 +1,22 @@ + + + + + + + + ${logPattern} + + + + + + + + + + + + + + diff --git a/modules/test/src/test/java/com/devonfw/module/test/common/base/BaseTestTest.java b/modules/test/src/test/java/com/devonfw/module/test/common/base/BaseTestTest.java new file mode 100644 index 00000000..b4a3cd1a --- /dev/null +++ b/modules/test/src/test/java/com/devonfw/module/test/common/base/BaseTestTest.java @@ -0,0 +1,48 @@ +package com.devonfw.module.test.common.base; + +import org.assertj.core.api.Assertions; +import org.junit.After; +import org.junit.Test; + +import com.devonfw.module.test.common.base.BaseTest; + +/** + * This test verifies the proper working of the {@link BaseTest} class. + * + * @author jmolinar + */ +public class BaseTestTest extends Assertions { + + private static boolean EXPECTED_RESULT; + + class MyTest extends BaseTest { + + @Override + protected void doSetUp() { + + assertThat(INITIALIZED).isEqualTo(EXPECTED_RESULT); + } + } + + @Test + public void testInitialization() { + + BaseTest test = new MyTest(); + // Set the expected result before running setUp()- + // Setup calls assertThat to test the expected result + EXPECTED_RESULT = false; + test.setUp(); + + EXPECTED_RESULT = true; + test.setUp(); + + } + + @After + public void tearDown() { + + // Just in case some test method will be added + MyTest.INITIALIZED = false; + } + +} diff --git a/modules/web/pom.xml b/modules/web/pom.xml new file mode 100644 index 00000000..312d2ce3 --- /dev/null +++ b/modules/web/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-modules + dev-SNAPSHOT + + com.devonfw.java.modules + devon4j-web + ${devon4j.version} + jar + ${project.artifactId} + Module for reusable web and servlet related code of the Open Application Standard Platform for Java (devon4j). + + + + javax.servlet + javax.servlet-api + provided + + + org.springframework + spring-web + + + net.sf.m-m-m + mmm-util-core + + + + com.devonfw.java.modules + devon4j-test + test + + + + \ No newline at end of file diff --git a/modules/web/src/main/java/com/devonfw/module/web/common/base/ToggleFilterWrapper.java b/modules/web/src/main/java/com/devonfw/module/web/common/base/ToggleFilterWrapper.java new file mode 100644 index 00000000..9e06d8e8 --- /dev/null +++ b/modules/web/src/main/java/com/devonfw/module/web/common/base/ToggleFilterWrapper.java @@ -0,0 +1,110 @@ +package com.devonfw.module.web.common.base; + +import java.io.IOException; + +import javax.annotation.PostConstruct; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class is responsible for wrapping a {@link Filter} and allows to be {@link #setEnabled(Boolean) disabled} e.g. + * for development tests (e.g. via an application property). In case the filter gets {@link #setEnabled(Boolean) + * disabled} a WARNING log message is produced and also written to {@link System#err}.
+ * + * Here is an example spring XML config from our sample application that allows to disable the CsrfFilter + * via an application property (enabled=false): + * + *
+ * <bean id="CsrfFilterWrapper" class="com.devonfw.module.web.common.base.ToggleFilterWrapper">
+ *   <property name="delegateFilter" ref="CsrfFilter"/>
+ *   <property name="enabled" value="${oasp.filter.csrf}"/>
+ * </bean>
+ * 
+ * + */ +public class ToggleFilterWrapper implements Filter { + + /** Logger instance. */ + private static final Logger LOG = LoggerFactory.getLogger(ToggleFilterWrapper.class); + + /** + * The delegated Filter. + */ + private Filter delegateFilter; + + /** + * Is set if this filter is enabled. + */ + private Boolean enabled = Boolean.FALSE; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + + } + + @PostConstruct + public void initialize() { + + if (!this.enabled) { + String message = + "****** FILTER " + this.delegateFilter + + " HAS BEEN DISABLED! THIS FEATURE SHOULD ONLY BE USED IN DEVELOPMENT MODE ******"; + LOG.warn(message); + // CHECKSTYLE:OFF (for development only) + System.err.println(message); + // CHECKSTYLE:ON + } + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, + ServletException { + + if (this.enabled) { + this.delegateFilter.doFilter(request, response, chain); + } else { + chain.doFilter(request, response); + } + } + + @Override + public void destroy() { + + } + + /** + * @param delegateFilter the filter to delegate to + */ + public void setDelegateFilter(Filter delegateFilter) { + + this.delegateFilter = delegateFilter; + } + + /** + * @param enabled the enabled flag + */ + public void setEnabled(Boolean enabled) { + + if (enabled != null) { + this.enabled = enabled; + } else { + LOG.warn(this.delegateFilter + " - ToggleFilterWrapper#setEnabled should not be set to NULL"); + } + } + + /** + * @return disabled + */ + public Boolean isEnabled() { + + return this.enabled; + } + +} diff --git a/modules/web/src/main/java/com/devonfw/module/web/common/base/debug/HttpEchoServlet.java b/modules/web/src/main/java/com/devonfw/module/web/common/base/debug/HttpEchoServlet.java new file mode 100644 index 00000000..6d96a4b5 --- /dev/null +++ b/modules/web/src/main/java/com/devonfw/module/web/common/base/debug/HttpEchoServlet.java @@ -0,0 +1,115 @@ +package com.devonfw.module.web.common.base.debug; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Enumeration; +import java.util.Locale; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * An {@link HttpServlet} to echo the {@link HttpServletRequest} with headers and payload as HTML output in the + * {@link HttpServletResponse}. This is very helpful especially if your application is behind a complex infrastructure + * with reverse-proxies, etc.
+ * In order to use this servlet simply add the following to your spring configuration (and adjust "/debug/echo/*" to + * your needs): + * + *
+ * @Bean
+ * public ServletRegistrationBean echoServletRegistration() {
+ *
+ *   HttpEchoServlet echoServlet = new HttpEchoServlet();
+ *   ServletRegistrationBean registration = new ServletRegistrationBean(echoServlet);
+ *   registration.addUrlMappings("/debug/echo/*");
+ *   return registration;
+ * }
+ * 
+ * + */ +public class HttpEchoServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private static final String SECURED = "****SECURED****"; + + private static final String COOKIE_SECURED = "=" + SECURED; + + /** + * Der Konstruktor. + */ + public HttpEchoServlet() { + super(); + } + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + String uri = request.getRequestURI(); + response.setContentType("text/html"); + PrintWriter out = response.getWriter(); + out.print("Debugausgabe von "); + out.print(uri); + out.println("

"); + out.print(request.getMethod()); + out.print(' '); + out.print(uri); + String query = request.getQueryString(); + if (query != null) { + out.print('?'); + out.print(query); + } + out.println(); + out.println("

Headers:

");
+    printHeaders(request, out);
+    out.println("

Payload:

");
+    BufferedReader in = request.getReader();
+    String line = in.readLine();
+    if (line == null) {
+      out.println("no data");
+    }
+    while (line != null) {
+      out.println(line);
+      line = in.readLine();
+    }
+    out.println("
"); + } + + private void printHeaders(HttpServletRequest request, PrintWriter out) { + + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String name = headerNames.nextElement(); + Enumeration headerValues = request.getHeaders(name); + while (headerValues.hasMoreElements()) { + String value = headerValues.nextElement(); + out.print(name); + out.print(": "); + out.print(getSecuredValue(name, value)); + out.println(); + } + } + } + + private String getSecuredValue(String originalHeaderName, String value) { + + String nameLowercase = originalHeaderName.toLowerCase(Locale.US); + if ("authorization".equals(nameLowercase)) { + return SECURED; + } + if ("proxy-authorization".equals(nameLowercase)) { + return SECURED; + } + if ("cookie".equals(nameLowercase)) { + if (value != null) { + return value.replaceAll("=[^; ]*", COOKIE_SECURED); + } + } + return value; + } + +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..daa3ee2d --- /dev/null +++ b/pom.xml @@ -0,0 +1,638 @@ + + + 4.0.0 + com.devonfw.java.dev + devon4j + dev-SNAPSHOT + pom + ${project.artifactId} + Open Application Standard Platform for Java (devon4j). + http://oasp.io/devon4j/maven/devon4j + 2014 + + + boms + modules + starters + templates + + + + 1.8 + UTF-8 + UTF-8 + 3.0.0-SNAPSHOT + oss + 2.0.4.RELEASE + + 3.0.1 + + + + + + org.apache.maven.plugins + maven-resources-plugin + + ${project.build.sourceEncoding} + true + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${project.build.sourceEncoding} + ${java.version} + ${java.version} + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + true + true + + + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + package + + jar-no-fork + + + + + + + + org.codehaus.mojo + flatten-maven-plugin + + ${devon4j.flatten.mode} + + + + flatten + process-test-resources + + flatten + + + + flatten.clean + clean + + clean + + + + + + + + org.apache.maven.plugins + maven-war-plugin + + WEB-INF/classes/config/application.properties,*.jsp + ${project.artifactId} + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + ${basedir} + + + + + + org.apache.maven.plugins + maven-site-plugin + + + + + org.jacoco + jacoco-maven-plugin + + + default-prepare-agent + + prepare-agent + + + + default-report + + report + + + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.0.2 + + + org.apache.maven.plugins + maven-compiler-plugin + 3.7.0 + + + org.apache.maven.plugins + maven-install-plugin + 2.5.2 + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + + org.apache.maven.plugins + maven-clean-plugin + 3.0.0 + + + org.apache.maven.plugins + maven-jar-plugin + 3.0.2 + + + org.apache.maven.plugins + maven-source-plugin + 3.0.1 + + + org.apache.maven.plugins + maven-site-plugin + 3.7 + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.0.0 + + + org.apache.maven.plugins + maven-antrun-plugin + 1.8 + + + org.apache.maven.plugins + maven-project-info-reports-plugin + 2.9 + + + org.apache.maven.plugins + maven-jxr-plugin + 2.5 + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.0.0 + + + + javax.interceptor + javax.interceptor-api + 1.2 + + + + + private + ${project.reporting.outputEncoding} + ${project.build.sourceEncoding} + true + + ${javadoc.option.doclint} + + + http://docs.oracle.com/javase/7/docs/api/ + http://m-m-m.sourceforge.net/apidocs/ + + JavaDocs for ${project.name} + JavaDocs for ${project.name} + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.20.1 + + + -Duser.language=en -Duser.region=EN + + ${basedir} + + + + org.apache.maven.plugins + maven-surefire-report-plugin + 2.20.1 + + + org.apache.maven.plugins + maven-pmd-plugin + 3.9.0 + + ${java.version} + + + + org.apache.maven.plugins + maven-war-plugin + 3.2.0 + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + org.apache.maven.plugins + maven-help-plugin + 2.2 + + + org.apache.maven.plugins + maven-archetype-plugin + ${maven.archetype.version} + + + org.codehaus.mojo + cobertura-maven-plugin + 2.7 + + + org.codehaus.mojo + sonar-maven-plugin + 3.4.0.905 + + + org.codehaus.mojo + findbugs-maven-plugin + 3.0.5 + + + org.codehaus.mojo + taglist-maven-plugin + 2.4 + + + org.codehaus.mojo + flatten-maven-plugin + 1.0.1 + + + org.codehaus.mojo + license-maven-plugin + 1.14 + + ${project.build.directory}/generated-resources + true + true + true + + + Apache Software License, Version 2.0|The Apache Software License, Version 2.0|Apache 2.0|Apache License, Version 2.0 + + + + + org.jacoco + jacoco-maven-plugin + 0.8.0 + + + org.owasp + dependency-check-maven + 3.1.1 + + + org.springframework.boot + spring-boot-maven-plugin + 2.0.4.RELEASE + + + + + + + + + org.apache.maven.plugins + maven-project-info-reports-plugin + + false + + + + org.apache.maven.plugins + maven-jxr-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + javax.interceptor + javax.interceptor-api + 1.2 + + + + private + ${project.reporting.outputEncoding} + ${project.build.sourceEncoding} + true + ${user.dir}/src/main/javadoc/stylesheet.css + + ${javadoc.option.doclint} + + + http://docs.oracle.com/javase/7/docs/api/ + + + JavaDocs for ${project.name} + JavaDocs for ${project.name} + + + + org.codehaus.mojo + taglist-maven-plugin + + + TODO + @todo + FIXME + @deprecated + REVIEW + + + + + org.owasp + dependency-check-maven + + + + aggregate + check + + + + + + org.codehaus.mojo + license-maven-plugin + + + + third-party-report + aggregate-third-party-report + + + + + + + + + + doclint-disabled + + [1.8,) + + + -Xdoclint:none + + + + security + + + + org.owasp + dependency-check-maven + + 8 + + + + + check + + + + + + + + + deploy + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + javax.interceptor + javax.interceptor-api + 1.2 + + + + + + attach-javadocs + + jar + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + + ${oasp.gpg.keyname} + + + + sign-artifacts + verify + + sign + + + + + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + licenses + + + + org.codehaus.mojo + license-maven-plugin + + + aggregate-download-licenses + generate-resources + + aggregate-download-licenses + + + + + aggregate-add-third-party + process-resources + + aggregate-add-third-party + + + + + + + + + + eclipse + + + eclipse.application + + + + eclipse-target + + + + + + GitHub + https://github.com/oasp/devon4j/issues + + + + https://github.com/oasp/devon4j/tree/develop + + + + + Devon4j-Team + http://oasp.io/devon4j/maven/team-list.html + + + + + hohwille + Jörg Hohwiller + hohwille@users.sourceforge.net + + + + admin + designer + developer + + +1 + + + + ksobkowiak + Krzysztof Sobkowiak + sobkowiak@onet.eu + + + + admin + designer + developer + + +1 + + + + + + + Apache Software Licenese + http://oasp.github.io/terms-of-use.html + repo + + + + + + + devon4j.releases + Devon4j Releases + http://oasp-ci.cloudapp.net/nexus/content/repositories/releases/ + + + devon4j.snapshots + Devon4j Snapshots + http://oasp-ci.cloudapp.net/nexus/content/repositories/snapshots/ + + + devon4j-site + file://${user.dir}/target/devon4j/maven + + + diff --git a/src/main/javadoc/stylesheet.css b/src/main/javadoc/stylesheet.css new file mode 100644 index 00000000..546edf93 --- /dev/null +++ b/src/main/javadoc/stylesheet.css @@ -0,0 +1,478 @@ +/* Javadoc style sheet */ +/* +Overall document style +*/ +body { + background-color:#ffffff; + color:#353833; + font-family:Arial, Helvetica, sans-serif; + font-size:76%; + margin:0; +} +a:link, a:visited { + text-decoration:none; + color:#4c6b87; +} +a:hover, a:focus { + text-decoration:none; + color:#bb7a2a; +} +a:active { + text-decoration:none; + color:#4c6b87; +} +a[name] { + color:#353833; +} +a[name]:hover { + text-decoration:none; + color:#353833; +} +pre { + font-size:1.3em; + white-space: pre-wrap; + background: #F6F6F6; + border-left: 5px solid #6CE26C; + padding: 0.5em; +} +h1 { + font-size:1.8em; +} +h2 { + font-size:1.5em; +} +h3 { + font-size:1.4em; +} +h4 { + font-size:1.3em; +} +h5 { + font-size:1.2em; +} +h6 { + font-size:1.1em; +} +ul { + list-style-type:disc; +} +code, tt { + font-size:1.2em; +} +dt code { + font-size:1.2em; +} +table tr td dt code { + font-size:1.2em; + vertical-align:top; +} +sup { + font-size:.6em; +} +/* +Document title and Copyright styles +*/ +.clear { + clear:both; + height:0px; + overflow:hidden; +} +.aboutLanguage { + float:right; + padding:0px 21px; + font-size:.8em; + z-index:200; + margin-top:-7px; +} +.legalCopy { + margin-left:.5em; +} +.bar a, .bar a:link, .bar a:visited, .bar a:active { + color:#FFFFFF; + text-decoration:none; +} +.bar a:hover, .bar a:focus { + color:#bb7a2a; +} +.tab { + background-color:#0066FF; + background-image:url(resources/titlebar.gif); + background-position:left top; + background-repeat:no-repeat; + color:#ffffff; + padding:8px; + width:5em; + font-weight:bold; +} +/* +Navigation bar styles +*/ +.bar { + background-image:url(resources/background.gif); + background-repeat:repeat-x; + color:#FFFFFF; + padding:.8em .5em .4em .8em; + height:auto;/*height:1.8em;*/ + font-size:1em; + margin:0; +} +.topNav { + background-image:url(resources/background.gif); + background-repeat:repeat-x; + color:#FFFFFF; + float:left; + padding:0; + width:100%; + clear:right; + height:2.8em; + padding-top:10px; + overflow:hidden; +} +.bottomNav { + margin-top:10px; + background-image:url(resources/background.gif); + background-repeat:repeat-x; + color:#FFFFFF; + float:left; + padding:0; + width:100%; + clear:right; + height:2.8em; + padding-top:10px; + overflow:hidden; +} +.subNav { + background-color:#dee3e9; + border-bottom:1px solid #9eadc0; + float:left; + width:100%; + overflow:hidden; +} +.subNav div { + clear:left; + float:left; + padding:0 0 5px 6px; +} +ul.navList, ul.subNavList { + float:left; + margin:0 25px 0 0; + padding:0; +} +ul.navList li{ + list-style:none; + float:left; + padding:3px 6px; +} +ul.subNavList li{ + list-style:none; + float:left; + font-size:90%; +} +.topNav a:link, .topNav a:active, .topNav a:visited, .bottomNav a:link, .bottomNav a:active, .bottomNav a:visited { + color:#FFFFFF; + text-decoration:none; +} +.topNav a:hover, .bottomNav a:hover { + text-decoration:none; + color:#bb7a2a; +} +.navBarCell1Rev { + background-image:url(resources/tab.gif); + background-color:#a88834; + color:#FFFFFF; + margin: auto 5px; + border:1px solid #c9aa44; +} +/* +Page header and footer styles +*/ +.header, .footer { + clear:both; + margin:0 20px; + padding:5px 0 0 0; +} +.indexHeader { + margin:10px; + position:relative; +} +.indexHeader h1 { + font-size:1.3em; +} +.title { + color:#2c4557; + margin:10px 0; +} +.subTitle { + margin:5px 0 0 0; +} +.header ul { + margin:0 0 25px 0; + padding:0; +} +.footer ul { + margin:20px 0 5px 0; +} +.header ul li, .footer ul li { + list-style:none; + font-size:1.2em; +} +/* +Heading styles +*/ +div.details ul.blockList ul.blockList ul.blockList li.blockList h4, div.details ul.blockList ul.blockList ul.blockListLast li.blockList h4 { + background-color:#dee3e9; + border-top:1px solid #9eadc0; + border-bottom:1px solid #9eadc0; + margin:0 0 6px -8px; + padding:2px 5px; +} +ul.blockList ul.blockList ul.blockList li.blockList h3 { + background-color:#dee3e9; + border-top:1px solid #9eadc0; + border-bottom:1px solid #9eadc0; + margin:0 0 6px -8px; + padding:2px 5px; +} +ul.blockList ul.blockList li.blockList h3 { + padding:0; + margin:15px 0; +} +ul.blockList li.blockList h2 { + padding:0px 0 20px 0; +} +/* +Page layout container styles +*/ +.contentContainer, .sourceContainer, .classUseContainer, .serializedFormContainer, .constantValuesContainer { + clear:both; + padding:10px 20px; + position:relative; +} +.indexContainer { + margin:10px; + position:relative; + font-size:1.0em; +} +.indexContainer h2 { + font-size:1.1em; + padding:0 0 3px 0; +} +.indexContainer ul { + margin:0; + padding:0; +} +.indexContainer ul li { + list-style:none; +} +.contentContainer .description dl dt, .contentContainer .details dl dt, .serializedFormContainer dl dt { + font-size:1.1em; + font-weight:bold; + margin:10px 0 0 0; + color:#4E4E4E; +} +.contentContainer .description dl dd, .contentContainer .details dl dd, .serializedFormContainer dl dd { + margin:10px 0 10px 20px; +} +.serializedFormContainer dl.nameValue dt { + margin-left:1px; + font-size:1.1em; + display:inline; + font-weight:bold; +} +.serializedFormContainer dl.nameValue dd { + margin:0 0 0 1px; + font-size:1.1em; + display:inline; +} +/* +List styles +*/ +ul.horizontal li { + display:inline; + font-size:0.9em; +} +ul.inheritance { + margin:0; + padding:0; +} +ul.inheritance li { + display:inline; + list-style:none; +} +ul.inheritance li ul.inheritance { + margin-left:15px; + padding-left:15px; + padding-top:1px; +} +ul.blockList, ul.blockListLast { + margin:10px 0 10px 0; + padding:0; +} +ul.blockList li.blockList, ul.blockListLast li.blockList { + list-style:none; + margin-bottom:25px; +} +ul.blockList ul.blockList li.blockList, ul.blockList ul.blockListLast li.blockList { + padding:0px 20px 5px 10px; + border:1px solid #9eadc0; + background-color:#f9f9f9; +} +ul.blockList ul.blockList ul.blockList li.blockList, ul.blockList ul.blockList ul.blockListLast li.blockList { + padding:0 0 5px 8px; + background-color:#ffffff; + border:1px solid #9eadc0; + border-top:none; +} +ul.blockList ul.blockList ul.blockList ul.blockList li.blockList { + margin-left:0; + padding-left:0; + padding-bottom:15px; + border:none; + border-bottom:1px solid #9eadc0; +} +ul.blockList ul.blockList ul.blockList ul.blockList li.blockListLast { + list-style:none; + border-bottom:none; + padding-bottom:0; +} +table tr td dl, table tr td dl dt, table tr td dl dd { + margin-top:0; + margin-bottom:1px; +} +/* +Table styles +*/ +.contentContainer table, .classUseContainer table, .constantValuesContainer table { + border-bottom:1px solid #9eadc0; + width:100%; +} +.contentContainer ul li table, .classUseContainer ul li table, .constantValuesContainer ul li table { + width:100%; +} +.contentContainer .description table, .contentContainer .details table { + border-bottom:none; +} +.contentContainer ul li table th.colOne, .contentContainer ul li table th.colFirst, .contentContainer ul li table th.colLast, .classUseContainer ul li table th, .constantValuesContainer ul li table th, .contentContainer ul li table td.colOne, .contentContainer ul li table td.colFirst, .contentContainer ul li table td.colLast, .classUseContainer ul li table td, .constantValuesContainer ul li table td{ + vertical-align:top; + padding-right:20px; +} +.contentContainer ul li table th.colLast, .classUseContainer ul li table th.colLast,.constantValuesContainer ul li table th.colLast, +.contentContainer ul li table td.colLast, .classUseContainer ul li table td.colLast,.constantValuesContainer ul li table td.colLast, +.contentContainer ul li table th.colOne, .classUseContainer ul li table th.colOne, +.contentContainer ul li table td.colOne, .classUseContainer ul li table td.colOne { + padding-right:3px; +} +.overviewSummary caption, .packageSummary caption, .contentContainer ul.blockList li.blockList caption, .summary caption, .classUseContainer caption, .constantValuesContainer caption { + position:relative; + text-align:left; + background-repeat:no-repeat; + color:#FFFFFF; + font-weight:bold; + clear:none; + overflow:hidden; + padding:0px; + margin:0px; +} +caption a:link, caption a:hover, caption a:active, caption a:visited { + color:#FFFFFF; +} +.overviewSummary caption span, .packageSummary caption span, .contentContainer ul.blockList li.blockList caption span, .summary caption span, .classUseContainer caption span, .constantValuesContainer caption span { + white-space:nowrap; + padding-top:8px; + padding-left:8px; + display:block; + float:left; + background-image:url(resources/titlebar.gif); + height:18px; +} +.overviewSummary .tabEnd, .packageSummary .tabEnd, .contentContainer ul.blockList li.blockList .tabEnd, .summary .tabEnd, .classUseContainer .tabEnd, .constantValuesContainer .tabEnd { + width:10px; + background-image:url(resources/titlebar_end.gif); + background-repeat:no-repeat; + background-position:top right; + position:relative; + float:left; +} +ul.blockList ul.blockList li.blockList table { + margin:0 0 12px 0px; + width:100%; +} +.tableSubHeadingColor { + background-color: #EEEEFF; +} +.altColor { + background-color:#eeeeef; +} +.rowColor { + background-color:#ffffff; +} +.overviewSummary td, .packageSummary td, .contentContainer ul.blockList li.blockList td, .summary td, .classUseContainer td, .constantValuesContainer td { + text-align:left; + padding:3px 3px 3px 7px; +} +th.colFirst, th.colLast, th.colOne, .constantValuesContainer th { + background:#dee3e9; + border-top:1px solid #9eadc0; + border-bottom:1px solid #9eadc0; + text-align:left; + padding:3px 3px 3px 7px; +} +td.colOne a:link, td.colOne a:active, td.colOne a:visited, td.colOne a:hover, td.colFirst a:link, td.colFirst a:active, td.colFirst a:visited, td.colFirst a:hover, td.colLast a:link, td.colLast a:active, td.colLast a:visited, td.colLast a:hover, .constantValuesContainer td a:link, .constantValuesContainer td a:active, .constantValuesContainer td a:visited, .constantValuesContainer td a:hover { + font-weight:bold; +} +td.colFirst, th.colFirst { + border-left:1px solid #9eadc0; + white-space:nowrap; +} +td.colLast, th.colLast { + border-right:1px solid #9eadc0; +} +td.colOne, th.colOne { + border-right:1px solid #9eadc0; + border-left:1px solid #9eadc0; +} +table.overviewSummary { + padding:0px; + margin-left:0px; +} +table.overviewSummary td.colFirst, table.overviewSummary th.colFirst, +table.overviewSummary td.colOne, table.overviewSummary th.colOne { + width:25%; + vertical-align:middle; +} +table.packageSummary td.colFirst, table.overviewSummary th.colFirst { + width:25%; + vertical-align:middle; +} +/* +Content styles +*/ +.description pre { + margin-top:0; +} +.deprecatedContent { + margin:0; + padding:10px 0; +} +.docSummary { + padding:0; +} +/* +Formatting effect styles +*/ +.sourceLineNo { + color:green; + padding:0 30px 0 0; +} +h1.hidden { + visibility:hidden; + overflow:hidden; + font-size:.9em; +} +.block { + display:block; + margin:3px 0 0 0; +} +.strong { + font-weight:bold; +} diff --git a/src/site/resources/favicon.ico b/src/site/resources/favicon.ico new file mode 100644 index 00000000..d34b8be3 Binary files /dev/null and b/src/site/resources/favicon.ico differ diff --git a/src/site/site.xml b/src/site/site.xml new file mode 100644 index 00000000..f0c2974e --- /dev/null +++ b/src/site/site.xml @@ -0,0 +1,31 @@ + + + + http://oasp.github.io/img/logo/oasp-logo-72dpi.png + http://oasp.github.io/ + + + + + + + + + + + + + + + + + + + + + + org.apache.maven.skins + maven-fluido-skin + 1.7 + + diff --git a/src/site/xdoc/index.xml b/src/site/xdoc/index.xml new file mode 100644 index 00000000..3abf3d44 --- /dev/null +++ b/src/site/xdoc/index.xml @@ -0,0 +1,13 @@ + + + + OASP4J - Open Application Standard Platform for Java + Jörg Hohwiller + + + +
+ Welcome to the Open Application Standard Platform for Java (OASP4J). +
+ +
diff --git a/starters/pom.xml b/starters/pom.xml new file mode 100644 index 00000000..de96e718 --- /dev/null +++ b/starters/pom.xml @@ -0,0 +1,99 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j + dev-SNAPSHOT + + devon4j-starters + pom + ${project.artifactId} + Spring-boot starters of the Open Application Standard Platform for Java (devon4j). + + + starter-cxf-client + starter-cxf-client-rest + starter-cxf-client-ws + starter-cxf-server + starter-cxf-server-rest + starter-cxf-server-ws + starter-spring-data-jpa + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + + com.devonfw.java.boms + devon4j-bom + ${devon4j.version} + pom + import + + + + + + + com.devonfw.java.modules + devon4j-test + test + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + javax.interceptor + javax.interceptor-api + 1.2 + + + + private + ${project.reporting.outputEncoding} + ${project.build.sourceEncoding} + true + ${user.dir}/src/main/javadoc/stylesheet.css + + + http://docs.oracle.com/javase/7/docs/api/ + http://oasp.github.io/devon4j/maven/apidocs/ + + JavaDocs for ${project.name} + JavaDocs for ${project.name} + + + + devon4j.javadoc + + javadoc + + + + devon4j.javadoc.aggregate + + aggregate + + + + + + + + diff --git a/starters/starter-cxf-client-rest/pom.xml b/starters/starter-cxf-client-rest/pom.xml new file mode 100644 index 00000000..b63e91b0 --- /dev/null +++ b/starters/starter-cxf-client-rest/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-starters + dev-SNAPSHOT + + com.devonfw.java.starters + devon4j-starter-cxf-client-rest + ${devon4j.version} + jar + ${project.artifactId} + Support for service clients based on Apache CXF. + + + + com.devonfw.java.starters + devon4j-starter-cxf-client + + + com.devonfw.java.modules + devon4j-cxf-client-rest + + + + diff --git a/starters/starter-cxf-client-rest/src/main/resources/META-INF/spring.factories b/starters/starter-cxf-client-rest/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..625b1dfb --- /dev/null +++ b/starters/starter-cxf-client-rest/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.devonfw.module.cxf.common.impl.client.rest.CxfRestClientAutoConfiguration \ No newline at end of file diff --git a/starters/starter-cxf-client-ws/pom.xml b/starters/starter-cxf-client-ws/pom.xml new file mode 100644 index 00000000..81fa845d --- /dev/null +++ b/starters/starter-cxf-client-ws/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-starters + dev-SNAPSHOT + + com.devonfw.java.starters + devon4j-starter-cxf-client-ws + ${devon4j.version} + jar + ${project.artifactId} + Support for service clients based on Apache CXF. + + + + com.devonfw.java.starters + devon4j-starter-cxf-client + + + com.devonfw.java.modules + devon4j-cxf-client-ws + + + + diff --git a/starters/starter-cxf-client-ws/src/main/resources/META-INF/spring.factories b/starters/starter-cxf-client-ws/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..67fb56f6 --- /dev/null +++ b/starters/starter-cxf-client-ws/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.devonfw.module.cxf.common.impl.client.ws.CxfWsClientAutoConfiguration \ No newline at end of file diff --git a/starters/starter-cxf-client/pom.xml b/starters/starter-cxf-client/pom.xml new file mode 100644 index 00000000..10ea8226 --- /dev/null +++ b/starters/starter-cxf-client/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-starters + dev-SNAPSHOT + + com.devonfw.java.starters + devon4j-starter-cxf-client + ${devon4j.version} + jar + ${project.artifactId} + Support for service clients based on Apache CXF. + + + + com.devonfw.java.modules + devon4j-cxf-client + + + + diff --git a/starters/starter-cxf-client/src/main/resources/META-INF/spring.factories b/starters/starter-cxf-client/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..99f6a529 --- /dev/null +++ b/starters/starter-cxf-client/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.devonfw.module.cxf.common.impl.client.CxfClientAutoConfiguration \ No newline at end of file diff --git a/starters/starter-cxf-server-rest/pom.xml b/starters/starter-cxf-server-rest/pom.xml new file mode 100644 index 00000000..88f844ee --- /dev/null +++ b/starters/starter-cxf-server-rest/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-starters + dev-SNAPSHOT + + com.devonfw.java.starters + devon4j-starter-cxf-server-rest + ${devon4j.version} + jar + ${project.artifactId} + Support for service server based on Apache CXF. + + + + com.devonfw.java.starters + devon4j-starter-cxf-server + + + com.devonfw.java.modules + devon4j-cxf-server-rest + + + + diff --git a/starters/starter-cxf-server-rest/src/main/resources/META-INF/spring.factories b/starters/starter-cxf-server-rest/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..0ae20fa7 --- /dev/null +++ b/starters/starter-cxf-server-rest/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.devonfw.module.cxf.common.impl.server.rest.CxfServerRestAutoConfiguration \ No newline at end of file diff --git a/starters/starter-cxf-server-ws/pom.xml b/starters/starter-cxf-server-ws/pom.xml new file mode 100644 index 00000000..87ec5cf7 --- /dev/null +++ b/starters/starter-cxf-server-ws/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-starters + dev-SNAPSHOT + + com.devonfw.java.starters + devon4j-starter-cxf-server-ws + ${devon4j.version} + jar + ${project.artifactId} + Support for service server based on Apache CXF. + + + + com.devonfw.java.starters + devon4j-starter-cxf-server + + + com.devonfw.java.modules + devon4j-cxf-server-ws + + + + diff --git a/starters/starter-cxf-server-ws/src/main/resources/META-INF/spring.factories b/starters/starter-cxf-server-ws/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..f11fd9d4 --- /dev/null +++ b/starters/starter-cxf-server-ws/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.devonfw.module.cxf.common.impl.server.soap.CxfServerSoapAutoConfiguration \ No newline at end of file diff --git a/starters/starter-cxf-server/pom.xml b/starters/starter-cxf-server/pom.xml new file mode 100644 index 00000000..d8e89bc2 --- /dev/null +++ b/starters/starter-cxf-server/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-starters + dev-SNAPSHOT + + com.devonfw.java.starters + devon4j-starter-cxf-server + ${devon4j.version} + jar + ${project.artifactId} + Support for service server based on Apache CXF. + + + + com.devonfw.java.modules + devon4j-cxf-server + + + + diff --git a/starters/starter-cxf-server/src/main/resources/META-INF/spring.factories b/starters/starter-cxf-server/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..e06cbdca --- /dev/null +++ b/starters/starter-cxf-server/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.devonfw.module.cxf.common.impl.server.CxfServerAutoConfiguration \ No newline at end of file diff --git a/starters/starter-spring-data-jpa/pom.xml b/starters/starter-spring-data-jpa/pom.xml new file mode 100644 index 00000000..eb89eb2a --- /dev/null +++ b/starters/starter-spring-data-jpa/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-starters + dev-SNAPSHOT + + com.devonfw.java.starters + devon4j-starter-spring-data-jpa + ${devon4j.version} + jar + ${project.artifactId} + Advanced spring-data support for Devonfw. + + + 1.8 + + + + + com.devonfw.java.modules + devon4j-jpa-spring-data + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.devonfw.java.modules + devon4j-test + test + + + com.h2database + h2 + test + + + + diff --git a/starters/starter-spring-data-jpa/src/main/resources/META-INF/spring.factories b/starters/starter-spring-data-jpa/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..19f19e64 --- /dev/null +++ b/starters/starter-spring-data-jpa/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.devonfw.module.jpa.dataaccess.api.DatabaseConfigProperties \ No newline at end of file diff --git a/starters/starter-spring-data-jpa/src/test/java/com/devonfw/example/TestApplication.java b/starters/starter-spring-data-jpa/src/test/java/com/devonfw/example/TestApplication.java new file mode 100644 index 00000000..3a740313 --- /dev/null +++ b/starters/starter-spring-data-jpa/src/test/java/com/devonfw/example/TestApplication.java @@ -0,0 +1,25 @@ +package com.devonfw.example; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import com.devonfw.module.jpa.dataaccess.impl.data.GenericRepositoryFactoryBean; + +/** + * Spring-boot app for testing. + */ +@SpringBootApplication +@EntityScan +@EnableJpaRepositories(repositoryFactoryBeanClass = GenericRepositoryFactoryBean.class) +public class TestApplication { + + /** + * @param args the command-line arguments + */ + public static void main(String[] args) { + + SpringApplication.run(TestApplication.class, args); + } +} diff --git a/starters/starter-spring-data-jpa/src/test/java/com/devonfw/example/component/common/api/Foo.java b/starters/starter-spring-data-jpa/src/test/java/com/devonfw/example/component/common/api/Foo.java new file mode 100644 index 00000000..77cf0445 --- /dev/null +++ b/starters/starter-spring-data-jpa/src/test/java/com/devonfw/example/component/common/api/Foo.java @@ -0,0 +1,15 @@ +package com.devonfw.example.component.common.api; + +import com.devonfw.example.general.common.api.TestApplicationEntity; + +public interface Foo extends TestApplicationEntity { + + String getMessage(); + + void setMessage(String message); + + String getName(); + + void setName(String name); + +} diff --git a/starters/starter-spring-data-jpa/src/test/java/com/devonfw/example/component/common/api/to/FooSearchCriteriaTo.java b/starters/starter-spring-data-jpa/src/test/java/com/devonfw/example/component/common/api/to/FooSearchCriteriaTo.java new file mode 100644 index 00000000..a4f5902e --- /dev/null +++ b/starters/starter-spring-data-jpa/src/test/java/com/devonfw/example/component/common/api/to/FooSearchCriteriaTo.java @@ -0,0 +1,48 @@ +package com.devonfw.example.component.common.api.to; + +import com.devonfw.module.basic.common.api.query.StringSearchConfigTo; + +/** + * {@link SearchCriteriaTo} to find instances of {@link com.devonfw.example.component.common.api.Foo}. + */ +public class FooSearchCriteriaTo extends SearchCriteriaTo { + + private static final long serialVersionUID = 1L; + + private String message; + + private StringSearchConfigTo messageOption; + + /** + * @return the string to search for {@link com.devonfw.example.component.common.api.Foo#getMessage() message}. + */ + public String getMessage() { + + return this.message; + } + + /** + * @param message new value of {@link #getMessage()}. + */ + public void setMessage(String message) { + + this.message = message; + } + + /** + * @return the {@link StringSearchConfigTo} used to search for {@link #getMessage() message}. + */ + public StringSearchConfigTo getMessageOption() { + + return this.messageOption; + } + + /** + * @param messageOption new value of {@link #getMessageOption()}. + */ + public void setMessageOption(StringSearchConfigTo messageOption) { + + this.messageOption = messageOption; + } + +} diff --git a/starters/starter-spring-data-jpa/src/test/java/com/devonfw/example/component/common/api/to/SearchCriteriaTo.java b/starters/starter-spring-data-jpa/src/test/java/com/devonfw/example/component/common/api/to/SearchCriteriaTo.java new file mode 100644 index 00000000..6ccc3d42 --- /dev/null +++ b/starters/starter-spring-data-jpa/src/test/java/com/devonfw/example/component/common/api/to/SearchCriteriaTo.java @@ -0,0 +1,31 @@ +package com.devonfw.example.component.common.api.to; + +import org.springframework.data.domain.Pageable; + +import com.devonfw.module.basic.common.api.to.AbstractTo; + +/** + * Abstract {@link AbstractTo TO} for search criteria. + */ +public abstract class SearchCriteriaTo extends AbstractTo { + + private static final long serialVersionUID = 1L; + + private Pageable pageable; + + /** + * @return the {@link Pageable} containing the optional pagination information or {@code null}. + */ + public Pageable getPageable() { + + return this.pageable; + } + + /** + * @param pageable new value of {@link #getPageable()}. + */ + public void setPageable(Pageable pageable) { + + this.pageable = pageable; + } +} diff --git a/starters/starter-spring-data-jpa/src/test/java/com/devonfw/example/component/dataaccess/api/FooEntity.java b/starters/starter-spring-data-jpa/src/test/java/com/devonfw/example/component/dataaccess/api/FooEntity.java new file mode 100644 index 00000000..7f55e582 --- /dev/null +++ b/starters/starter-spring-data-jpa/src/test/java/com/devonfw/example/component/dataaccess/api/FooEntity.java @@ -0,0 +1,45 @@ +package com.devonfw.example.component.dataaccess.api; + +import javax.persistence.Entity; +import javax.persistence.Table; + +import com.devonfw.example.component.common.api.Foo; +import com.devonfw.example.general.dataaccess.api.TestApplicationPersistenceEntity; + +/** + * Implementation of {@link Foo} as {@link TestApplicationPersistenceEntity persistence entity}. + */ +@Entity +@Table(name = "Bar") +public class FooEntity extends TestApplicationPersistenceEntity implements Foo { + private static final long serialVersionUID = 1L; + + private String message; + + private String name; + + @Override + public String getMessage() { + + return this.message; + } + + @Override + public void setMessage(String message) { + + this.message = message; + } + + @Override + public String getName() { + + return this.name; + } + + @Override + public void setName(String name) { + + this.name = name; + } + +} diff --git a/starters/starter-spring-data-jpa/src/test/java/com/devonfw/example/component/dataaccess/api/FooRepository.java b/starters/starter-spring-data-jpa/src/test/java/com/devonfw/example/component/dataaccess/api/FooRepository.java new file mode 100644 index 00000000..17597e02 --- /dev/null +++ b/starters/starter-spring-data-jpa/src/test/java/com/devonfw/example/component/dataaccess/api/FooRepository.java @@ -0,0 +1,45 @@ +package com.devonfw.example.component.dataaccess.api; + +import static com.querydsl.core.alias.Alias.$; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import com.devonfw.example.component.common.api.Foo; +import com.devonfw.example.component.common.api.to.FooSearchCriteriaTo; +import com.devonfw.module.jpa.dataaccess.api.QueryUtil; +import com.devonfw.module.jpa.dataaccess.api.data.DefaultRepository; +import com.querydsl.jpa.impl.JPAQuery; + +/** + * {@link DefaultRepository} for {@link FooEntity}. + */ +public interface FooRepository extends DefaultRepository { + + /** + * @param message the {@link Foo#getMessage() message} to match. + * @param pageable the {@link Pageable} to configure the paging. + * @return the {@link Page} of {@link FooEntity} objects that matched the search. + */ + @Query("SELECT foo FROM FooEntity foo" // + + " WHERE foo.message = :message") + Page findByMessage(@Param("message") String message, Pageable pageable); + + /** + * @param criteria the {@link FooSearchCriteriaTo} with the criteria to search. + * @return the {@link Page} of the {@link FooEntity} objects that matched the search. + */ + default Page findByCriteria(FooSearchCriteriaTo criteria) { + + FooEntity alias = newDslAlias(); + JPAQuery query = newDslQuery(alias); + String message = criteria.getMessage(); + if ((message != null) && !message.isEmpty()) { + QueryUtil.get().whereString(query, $(alias.getMessage()), message, criteria.getMessageOption()); + } + return QueryUtil.get().findPaginated(criteria.getPageable(), query, false); + } + +} diff --git a/starters/starter-spring-data-jpa/src/test/java/com/devonfw/example/general/common/api/TestApplicationEntity.java b/starters/starter-spring-data-jpa/src/test/java/com/devonfw/example/general/common/api/TestApplicationEntity.java new file mode 100644 index 00000000..98ae76a9 --- /dev/null +++ b/starters/starter-spring-data-jpa/src/test/java/com/devonfw/example/general/common/api/TestApplicationEntity.java @@ -0,0 +1,11 @@ +package com.devonfw.example.general.common.api; + +import net.sf.mmm.util.entity.api.MutableGenericEntity; + +/** + * This is the abstract interface for a {@link MutableGenericEntity} of this application. We are using {@link Long} for + * all {@link #getId() primary keys}. + */ +public abstract interface TestApplicationEntity extends MutableGenericEntity { + +} diff --git a/starters/starter-spring-data-jpa/src/test/java/com/devonfw/example/general/dataaccess/api/TestApplicationPersistenceEntity.java b/starters/starter-spring-data-jpa/src/test/java/com/devonfw/example/general/dataaccess/api/TestApplicationPersistenceEntity.java new file mode 100644 index 00000000..2426da63 --- /dev/null +++ b/starters/starter-spring-data-jpa/src/test/java/com/devonfw/example/general/dataaccess/api/TestApplicationPersistenceEntity.java @@ -0,0 +1,108 @@ +package com.devonfw.example.general.dataaccess.api; + +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.MappedSuperclass; +import javax.persistence.Transient; +import javax.persistence.Version; + +import com.devonfw.example.general.common.api.TestApplicationEntity; +import com.devonfw.module.jpa.dataaccess.api.MutablePersistenceEntity; + +/** + * Abstract Entity for all Entities with an id and a version field. + * + */ +@MappedSuperclass +public abstract class TestApplicationPersistenceEntity implements TestApplicationEntity, MutablePersistenceEntity { + + private static final long serialVersionUID = 1L; + + /** @see #getId() */ + private Long id; + + /** @see #getModificationCounter() */ + private int modificationCounter; + + /** @see #getRevision() */ + private Number revision; + + /** + * The constructor. + */ + public TestApplicationPersistenceEntity() { + + super(); + } + + @Override + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + public Long getId() { + + return this.id; + } + + /** + * {@inheritDoc} + */ + @Override + public void setId(Long id) { + + this.id = id; + } + + @Override + @Version + public int getModificationCounter() { + + return this.modificationCounter; + } + + @Override + public void setModificationCounter(int version) { + + this.modificationCounter = version; + } + + @Override + @Transient + public Number getRevision() { + + return this.revision; + } + + /** + * @param revision the revision to set + */ + @Override + public void setRevision(Number revision) { + + this.revision = revision; + } + + @Override + public String toString() { + + StringBuilder buffer = new StringBuilder(); + toString(buffer); + return buffer.toString(); + } + + /** + * Method to extend {@link #toString()} logic. + * + * @param buffer is the {@link StringBuilder} where to {@link StringBuilder#append(Object) append} the string + * representation. + */ + protected void toString(StringBuilder buffer) { + + buffer.append(getClass().getSimpleName()); + if (this.id != null) { + buffer.append("[id="); + buffer.append(this.id); + buffer.append("]"); + } + } +} \ No newline at end of file diff --git a/starters/starter-spring-data-jpa/src/test/java/com/devonfw/module/jpa/dataaccess/api/DefaultRepositoryTest.java b/starters/starter-spring-data-jpa/src/test/java/com/devonfw/module/jpa/dataaccess/api/DefaultRepositoryTest.java new file mode 100644 index 00000000..d604e7ba --- /dev/null +++ b/starters/starter-spring-data-jpa/src/test/java/com/devonfw/module/jpa/dataaccess/api/DefaultRepositoryTest.java @@ -0,0 +1,217 @@ +package com.devonfw.module.jpa.dataaccess.api; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.transaction.Transactional; + +import org.junit.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.jpa.domain.JpaSort; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +import com.devonfw.example.TestApplication; +import com.devonfw.example.component.common.api.to.FooSearchCriteriaTo; +import com.devonfw.example.component.dataaccess.api.FooEntity; +import com.devonfw.example.component.dataaccess.api.FooRepository; +import com.devonfw.module.basic.common.api.query.LikePatternSyntax; +import com.devonfw.module.basic.common.api.query.StringSearchConfigTo; +import com.devonfw.module.basic.common.api.query.StringSearchOperator; +import com.devonfw.module.test.common.base.ComponentTest; + +/** + * Test of {@link FooRepository} in order to test the following infrastructure: + *
    + *
  • {@link com.devonfw.module.jpa.dataaccess.api.data.DefaultRepository}
  • + *
  • {@link com.devonfw.module.jpa.dataaccess.api.data.GenericRepository}
  • + *
  • {@link com.devonfw.module.jpa.dataaccess.impl.data.GenericRepositoryFactoryBean}
  • + *
  • {@link com.devonfw.module.jpa.dataaccess.impl.data.GenericRepositoryImpl}
  • + *
+ */ +@SpringBootTest(classes = { TestApplication.class }, webEnvironment = WebEnvironment.NONE) +public class DefaultRepositoryTest extends ComponentTest { + + @Inject + private FooRepository fooRepository; + + @PersistenceContext + private EntityManager entityManager; + + @Inject + private TransactionTemplate template; + + private T doInTx(TransactionCallback lambda) { + + return this.template.execute(lambda); + } + + /** + * Test of {@link com.devonfw.module.jpa.dataaccess.api.data.GenericRepository#forceIncrementModificationCounter(Object)}. + * Ensures that the modification counter is updated after the call of that method when the transaction is closed. + */ + @Test + public void testForceIncrementModificationCounter() { + + // given + FooEntity entity = doInTx((x) -> newFoo("Foo")); + + // when + FooEntity entity2 = doInTx((x) -> { + FooEntity foo = this.fooRepository.find(entity.getId()); + this.fooRepository.forceIncrementModificationCounter(foo); + return foo; + }); + + // then + assertThat(entity.getModificationCounter()).isEqualTo(0); + assertThat(entity2.getModificationCounter()).isEqualTo(1); + } + + private FooEntity newFoo(String message) { + + return newFoo(message, message); + } + + private FooEntity newFoo(String message, String name) { + + FooEntity entity = new FooEntity(); + entity.setMessage(message); + entity.setName(name); + this.fooRepository.save(entity); + assertThat(entity.getId()).isNotNull(); + assertThat(entity.getModificationCounter()).isEqualTo(0); + assertThat(this.fooRepository.find(entity.getId())).isSameAs(entity); + return entity; + } + + /** + * Test of {@link com.devonfw.module.jpa.dataaccess.api.data.GenericRepository#forceIncrementModificationCounter(Object)}. + * Ensures that the modification counter is updated after the call of that method when the transaction is closed. + */ + @Test + @Transactional + public void testFindByMessage() { + + this.fooRepository.deleteAll(); + + // given + int hitsPerPage = 5; + int totalPages = 3; + int pageNumber = totalPages - 1; + String message = "Foo"; + int totalElements = hitsPerPage * totalPages - 1; + for (int i = 0; i < totalElements; i++) { + newFoo(message, createName(i)); + } + Sort sort = JpaSort.unsafe(Direction.ASC, "name"); + Pageable pageable = new PageRequest(pageNumber, hitsPerPage, sort); + + // when + Page page = this.fooRepository.findByMessage(message, pageable); + + // then + assertThat(page.getNumber()).isEqualTo(pageNumber); + assertThat(page.getNumberOfElements()).isEqualTo(hitsPerPage - 1); + assertThat(page.getSize()).isEqualTo(hitsPerPage); + assertThat(page.getTotalPages()).isEqualTo(totalPages); + assertThat(page.getTotalElements()).isEqualTo(totalElements); + assertThat(page.getContent()).hasSize(page.getNumberOfElements()); + int i = pageNumber * hitsPerPage; + for (FooEntity foo : page.getContent()) { + assertThat(foo.getName()).isEqualTo(createName(i)); + i++; + } + } + + private String createName(int i) { + + return "Name" + String.format("%03d", i); + } + + /** + * Test of {@link com.devonfw.module.jpa.dataaccess.api.data.GenericRepository#forceIncrementModificationCounter(Object)}. + * Ensures that the modification counter is updated after the call of that method when the transaction is closed. + */ + @Test + @Transactional + public void testFindByCriteria() { + + this.fooRepository.deleteAll(); + + // given + FooSearchCriteriaTo criteria = new FooSearchCriteriaTo(); + criteria.setMessage("T?st"); + StringSearchConfigTo config = StringSearchConfigTo.of(LikePatternSyntax.GLOB); + config.setIgnoreCase(true); + config.setMatchSubstring(true); + criteria.setMessageOption(config); + PageRequest pageable = new PageRequest(0, 100, new Sort(Direction.DESC, "message")); + criteria.setPageable(pageable); + List values = new ArrayList<>(Arrays.asList("MY_TEST", "Sometest", "Test", "Testing", "Xtest")); + Collections.shuffle(values); + for (String message : values) { + newFoo(message); + } + newFoo("Aaa"); + newFoo("Xxx"); + + // when + Page hits = this.fooRepository.findByCriteria(criteria); + + // then + Collections.sort(values, (x, y) -> -x.compareTo(y)); + assertThat(hits.getContent().stream().map(x -> x.getMessage()).collect(Collectors.toList())) + .containsExactlyElementsOf(values); + + // and when + config.setOperator(StringSearchOperator.NOT_LIKE); + hits = this.fooRepository.findByCriteria(criteria); + + // then + assertThat(hits.getContent().stream().map(x -> x.getMessage()).collect(Collectors.toList())).containsExactly("Xxx", + "Aaa"); + + // and when + config.setOperator(StringSearchOperator.LIKE); + config.setIgnoreCase(false); + hits = this.fooRepository.findByCriteria(criteria); + + // then + assertThat(hits.getContent().stream().map(x -> x.getMessage()).collect(Collectors.toList())) + .containsExactly("Testing", "Test"); + + // and when + config.setMatchSubstring(false); + hits = this.fooRepository.findByCriteria(criteria); + + // then + assertThat(hits.getContent().stream().map(x -> x.getMessage()).collect(Collectors.toList())) + .containsExactly("Test"); + + // and when + criteria.setMessage("Test"); + config.setLikeSyntax(null); + config.setOperator(StringSearchOperator.NE); + pageable = new PageRequest(0, 100, new Sort(Direction.ASC, "message")); + criteria.setPageable(pageable); + hits = this.fooRepository.findByCriteria(criteria); + + // then + assertThat(hits.getContent().stream().map(x -> x.getMessage()).collect(Collectors.toList())).containsExactly("Aaa", + "MY_TEST", "Sometest", "Testing", "Xtest", "Xxx"); + } + +}; diff --git a/starters/starter-spring-data-jpa/src/test/resources/config/application.properties b/starters/starter-spring-data-jpa/src/test/resources/config/application.properties new file mode 100644 index 00000000..3e4a2961 --- /dev/null +++ b/starters/starter-spring-data-jpa/src/test/resources/config/application.properties @@ -0,0 +1,2 @@ +# https://docs.spring.io/spring-boot/docs/current/reference/html/howto-embedded-servlet-containers.html#howto-user-a-random-unassigned-http-port +database.query.in-clause.max-values=1000 diff --git a/templates/pom.xml b/templates/pom.xml new file mode 100644 index 00000000..06c89cef --- /dev/null +++ b/templates/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j + dev-SNAPSHOT + + devon4j-templates + pom + ${project.artifactId} + Templates (maven archetypes) of the Open Application Standard Platform for Java (devon4j). + + + server + + + diff --git a/templates/server/pom.xml b/templates/server/pom.xml new file mode 100644 index 00000000..b63347e1 --- /dev/null +++ b/templates/server/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + com.devonfw.java.dev + devon4j-templates + dev-SNAPSHOT + + com.devonfw.java.templates + devon4j-template-server + ${devon4j.version} + maven-archetype + ${project.artifactId} + Application template for the server of the Open Application Standard Platform for Java (devon4j). + + + + + ${basedir}/src/main/resources/ + true + + + target/archetype + + + org.apache.maven.archetype + archetype-packaging + ${maven.archetype.version} + + + + + + org.apache.maven.plugins + maven-resources-plugin + + + $[*] + + false + + + + org.apache.maven.plugins + maven-archetype-plugin + + + + + diff --git a/templates/server/src/main/resources/META-INF/maven/archetype-metadata.xml b/templates/server/src/main/resources/META-INF/maven/archetype-metadata.xml new file mode 100644 index 00000000..0a716cb9 --- /dev/null +++ b/templates/server/src/main/resources/META-INF/maven/archetype-metadata.xml @@ -0,0 +1,84 @@ + + + oasp4j-template-server + + + + . + + + h2 + + + + . + batch|[.] + + + + + + + api + + **/*.* + + + + core + + **/*.* + + + + __earProjectName__ + + **/*.* + + + + + __batch__ + + **/*.* + + + + + + + + src/main/java + + + src/main/resources + + + src/test/java + + + src/test/resources + + + src/main/webapp + + + + + diff --git a/templates/server/src/main/resources/archetype-resources/__batch__/pom.xml b/templates/server/src/main/resources/archetype-resources/__batch__/pom.xml new file mode 100644 index 00000000..a8f800b7 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/__batch__/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + ${groupId} + ${rootArtifactId} + ${version} + + ${rootArtifactId}-batch + jar + ${project.artifactId} + Batches for the ${rootArtifactId} application. + + + + com.devonfw.java.modules + devon4j-batch + + + + ${project.groupId} + ${rootArtifactId}-core + ${project.version} + + + + + org.springframework.batch + spring-batch-test + test + + + + com.devonfw.java.modules + devon4j-test + test + + + + + + + src/main/resources + true + + + + + org.apache.maven.plugins + maven-jar-plugin + + + config/application.properties + + + + + + + diff --git a/templates/server/src/main/resources/archetype-resources/__batch__/src/main/java/__packageInPathFormat__/SpringBootBatchApp.java b/templates/server/src/main/resources/archetype-resources/__batch__/src/main/java/__packageInPathFormat__/SpringBootBatchApp.java new file mode 100644 index 00000000..7e5ed051 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/__batch__/src/main/java/__packageInPathFormat__/SpringBootBatchApp.java @@ -0,0 +1,31 @@ +package ${package}; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; + +import com.devonfw.module.jpa.dataaccess.api.AdvancedRevisionEntity; + +/** + * {@link SpringBootApplication} for running this batch. + */ +@SpringBootApplication(exclude = { EndpointAutoConfiguration.class, SecurityAutoConfiguration.class, +SecurityFilterAutoConfiguration.class, }) +@EntityScan(basePackages = { "${package}" }, basePackageClasses = { AdvancedRevisionEntity.class }) +@EnableGlobalMethodSecurity(jsr250Enabled = false) +public class SpringBootBatchApp { + + /** + * Entry point for spring-boot based app + * + * @param args - arguments + */ + public static void main(String[] args) { + + SpringApplication.run(SpringBootBatchApp.class, args); + } +} diff --git a/templates/server/src/main/resources/archetype-resources/__batch__/src/main/java/__packageInPathFormat__/general/batch/impl/config/BeansBatchConfig.java b/templates/server/src/main/resources/archetype-resources/__batch__/src/main/java/__packageInPathFormat__/general/batch/impl/config/BeansBatchConfig.java new file mode 100644 index 00000000..ee2ccea4 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/__batch__/src/main/java/__packageInPathFormat__/general/batch/impl/config/BeansBatchConfig.java @@ -0,0 +1,193 @@ +package ${package}.general.batch.impl.config; + +import javax.inject.Inject; +import javax.sql.DataSource; + +import org.springframework.batch.core.configuration.support.JobRegistryBeanPostProcessor; +import org.springframework.batch.core.configuration.support.MapJobRegistry; +import org.springframework.batch.core.explore.support.JobExplorerFactoryBean; +import org.springframework.batch.core.launch.support.RunIdIncrementer; +import org.springframework.batch.core.launch.support.SimpleJobOperator; +import org.springframework.batch.core.repository.support.JobRepositoryFactoryBean; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; +import org.springframework.transaction.PlatformTransactionManager; + +/** + * This class contains the configuration like jobLauncher,Jobrepository etc. + */ +import com.devonfw.module.batch.common.impl.JobLauncherWithAdditionalRestartCapabilities; + +/** + * This class contains configuration of batch beans. + */ +@Configuration +public class BeansBatchConfig { + + private JobRepositoryFactoryBean jobRepository; + + private MapJobRegistry jobRegistry; + + private JobLauncherWithAdditionalRestartCapabilities jobLauncher; + + private JobExplorerFactoryBean jobExplorer; + + private DataSource dataSource; + + private PlatformTransactionManager transactionManager; + + @Value("ISOLATION_DEFAULT") + private String isolationLevelForCreate; + + /** + * This method is creating joboperator bean + * + * @return SimpleJobOperator + */ + @Bean + @DependsOn({ "jobRepository", "jobExplorer", "jobRegistry", "jobLauncher" }) + public SimpleJobOperator jobOperator() { + + SimpleJobOperator jobOperator = new SimpleJobOperator(); + try { + jobOperator.setJobExplorer(this.jobExplorer.getObject()); + } catch (Exception e) { + throw new BeanCreationException("Could not create BatchJobOperator", e); + } + + jobOperator.setJobLauncher(this.jobLauncher); + jobOperator.setJobRegistry(this.jobRegistry); + + try { + jobOperator.setJobRepository(this.jobRepository.getObject()); + } catch (Exception e) { + throw new BeanCreationException("Could not create BatchJobOperator", e); + } + + return jobOperator; + } + + /** + * This method is creating jobrepository + * + * @return JobRepositoryFactoryBean + */ + @Bean(name = "jobRepository") + public JobRepositoryFactoryBean jobRepository() { + + this.jobRepository = new JobRepositoryFactoryBean(); + this.jobRepository.setDataSource(this.dataSource); + this.jobRepository.setTransactionManager(this.transactionManager); + this.jobRepository.setIsolationLevelForCreate(this.isolationLevelForCreate); + return this.jobRepository; + } + + /** + * This method is creating jobLauncher bean + * + * @return SimpleJobLauncher + */ + @Bean + @DependsOn("jobRepository") + public JobLauncherWithAdditionalRestartCapabilities jobLauncher() { + + this.jobLauncher = new JobLauncherWithAdditionalRestartCapabilities(); + + try { + this.jobLauncher.setJobRepository(this.jobRepository.getObject()); + } catch (Exception e) { + throw new BeanCreationException("Could not create BatchJobOperator", e); + } + + return this.jobLauncher; + } + + /** + * This method is creating jobExplorer bean + * + * @return JobExplorerFactoryBean + */ + @Bean + @DependsOn("dataSource") + public JobExplorerFactoryBean jobExplorer() { + + this.jobExplorer = new JobExplorerFactoryBean(); + this.jobExplorer.setDataSource(this.dataSource); + return this.jobExplorer; + } + + /** + * This method is creating jobRegistry bean + * + * @return MapJobRegistry + */ + @Bean + public MapJobRegistry jobRegistry() { + + this.jobRegistry = new MapJobRegistry(); + return this.jobRegistry; + } + + /** + * This method is creating JobRegistryBeanPostProcessor + * + * @return JobRegistryBeanPostProcessor + */ + @Bean + @DependsOn("jobRegistry") + public JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor() { + + JobRegistryBeanPostProcessor postProcessor = new JobRegistryBeanPostProcessor(); + postProcessor.setJobRegistry(this.jobRegistry); + return postProcessor; + } + + /** + * This method is creating incrementer + * + * @return RunIdIncrementer + */ + @Bean + public RunIdIncrementer incrementer() { + + return new RunIdIncrementer(); + } + + /** + * @return datasource + */ + public DataSource getDataSource() { + + return this.dataSource; + } + + /** + * @param datasource the datasource to set + */ + @Inject + public void setDataSource(DataSource datasource) { + + this.dataSource = datasource; + } + + /** + * @return transactionManager + */ + public PlatformTransactionManager getTransactionManager() { + + return this.transactionManager; + } + + /** + * @param transactionManager the transactionManager to set + */ + @Inject + public void setTransactionManager(PlatformTransactionManager transactionManager) { + + this.transactionManager = transactionManager; + } + +} diff --git a/templates/server/src/main/resources/archetype-resources/__batch__/src/main/resources/db/migration/h2/V0002__Add_batch_tables.sql b/templates/server/src/main/resources/archetype-resources/__batch__/src/main/resources/db/migration/h2/V0002__Add_batch_tables.sql new file mode 100644 index 00000000..980d37d4 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/__batch__/src/main/resources/db/migration/h2/V0002__Add_batch_tables.sql @@ -0,0 +1,81 @@ +-- Autogenerated: do not edit this file + +CREATE TABLE BATCH_JOB_INSTANCE ( + JOB_INSTANCE_ID BIGINT IDENTITY NOT NULL PRIMARY KEY , + VERSION BIGINT , + JOB_NAME VARCHAR(100) NOT NULL, + JOB_KEY VARCHAR(32) NOT NULL, + constraint JOB_INST_UN unique (JOB_NAME, JOB_KEY) +) ; + +CREATE TABLE BATCH_JOB_EXECUTION ( + JOB_EXECUTION_ID BIGINT IDENTITY NOT NULL PRIMARY KEY , + VERSION BIGINT , + JOB_INSTANCE_ID BIGINT NOT NULL, + CREATE_TIME TIMESTAMP NOT NULL, + START_TIME TIMESTAMP DEFAULT NULL , + END_TIME TIMESTAMP DEFAULT NULL , + STATUS VARCHAR(10) , + EXIT_CODE VARCHAR(2500) , + EXIT_MESSAGE VARCHAR(2500) , + LAST_UPDATED TIMESTAMP, + JOB_CONFIGURATION_LOCATION VARCHAR(2500) NULL, + constraint JOB_INST_EXEC_FK foreign key (JOB_INSTANCE_ID) + references BATCH_JOB_INSTANCE(JOB_INSTANCE_ID) +) ; + +CREATE TABLE BATCH_JOB_EXECUTION_PARAMS ( + JOB_EXECUTION_ID BIGINT NOT NULL , + TYPE_CD VARCHAR(6) NOT NULL , + KEY_NAME VARCHAR(100) NOT NULL , + STRING_VAL VARCHAR(250) , + DATE_VAL TIMESTAMP DEFAULT NULL , + LONG_VAL BIGINT , + DOUBLE_VAL DOUBLE PRECISION , + IDENTIFYING CHAR(1) NOT NULL , + constraint JOB_EXEC_PARAMS_FK foreign key (JOB_EXECUTION_ID) + references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID) +) ; + +CREATE TABLE BATCH_STEP_EXECUTION ( + STEP_EXECUTION_ID BIGINT IDENTITY NOT NULL PRIMARY KEY , + VERSION BIGINT NOT NULL, + STEP_NAME VARCHAR(100) NOT NULL, + JOB_EXECUTION_ID BIGINT NOT NULL, + START_TIME TIMESTAMP NOT NULL , + END_TIME TIMESTAMP DEFAULT NULL , + STATUS VARCHAR(10) , + COMMIT_COUNT BIGINT , + READ_COUNT BIGINT , + FILTER_COUNT BIGINT , + WRITE_COUNT BIGINT , + READ_SKIP_COUNT BIGINT , + WRITE_SKIP_COUNT BIGINT , + PROCESS_SKIP_COUNT BIGINT , + ROLLBACK_COUNT BIGINT , + EXIT_CODE VARCHAR(2500) , + EXIT_MESSAGE VARCHAR(2500) , + LAST_UPDATED TIMESTAMP, + constraint JOB_EXEC_STEP_FK foreign key (JOB_EXECUTION_ID) + references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID) +) ; + +CREATE TABLE BATCH_STEP_EXECUTION_CONTEXT ( + STEP_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY, + SHORT_CONTEXT VARCHAR(2500) NOT NULL, + SERIALIZED_CONTEXT LONGVARCHAR , + constraint STEP_EXEC_CTX_FK foreign key (STEP_EXECUTION_ID) + references BATCH_STEP_EXECUTION(STEP_EXECUTION_ID) +) ; + +CREATE TABLE BATCH_JOB_EXECUTION_CONTEXT ( + JOB_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY, + SHORT_CONTEXT VARCHAR(2500) NOT NULL, + SERIALIZED_CONTEXT LONGVARCHAR , + constraint JOB_EXEC_CTX_FK foreign key (JOB_EXECUTION_ID) + references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID) +) ; + +CREATE SEQUENCE BATCH_STEP_EXECUTION_SEQ; +CREATE SEQUENCE BATCH_JOB_EXECUTION_SEQ; +CREATE SEQUENCE BATCH_JOB_SEQ; diff --git a/templates/server/src/main/resources/archetype-resources/__batch__/src/test/java/__packageInPathFormat__/general/batch/base/test/SpringBatchIntegrationTest.java b/templates/server/src/main/resources/archetype-resources/__batch__/src/test/java/__packageInPathFormat__/general/batch/base/test/SpringBatchIntegrationTest.java new file mode 100644 index 00000000..6bea2912 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/__batch__/src/test/java/__packageInPathFormat__/general/batch/base/test/SpringBatchIntegrationTest.java @@ -0,0 +1,53 @@ +package ${package}.general.batch.base.test; + +import javax.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.test.JobLauncherTestUtils; + +import ${package}.general.common.base.test.TestUtil; +import com.devonfw.module.test.common.base.ComponentTest; + +/** + * Base class for all spring batch integration tests. It helps to do End-to-End job tests. + */ +public abstract class SpringBatchIntegrationTest extends ComponentTest { + + @Inject + private JobLauncher jobLauncher; + + @Inject + private Flyway flyway; + + @Override + protected void doSetUp() { + + super.doSetUp(); + this.flyway.clean(); + this.flyway.migrate(); + } + + @Override + protected void doTearDown() { + + super.doTearDown(); + TestUtil.logout(); + } + + /** + * @param job job to configure + * @return jobLauncherTestUtils + */ + public JobLauncherTestUtils getJobLauncherTestUtils(Job job) { + + JobLauncherTestUtils jobLauncherTestUtils = new JobLauncherTestUtils(); + jobLauncherTestUtils.setJob(job); + jobLauncherTestUtils.setJobLauncher(this.jobLauncher); + + return jobLauncherTestUtils; + } +} diff --git a/templates/server/src/main/resources/archetype-resources/__batch__/src/test/java/__packageInPathFormat__/general/common/base/test/TestUtil.java b/templates/server/src/main/resources/archetype-resources/__batch__/src/test/java/__packageInPathFormat__/general/common/base/test/TestUtil.java new file mode 100644 index 00000000..a3fd2687 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/__batch__/src/test/java/__packageInPathFormat__/general/common/base/test/TestUtil.java @@ -0,0 +1,29 @@ +package ${package}.general.common.base.test; + +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * This is a utility for testing. It allows to simulate authentication for component testing. + */ +public class TestUtil { + + /** + * @param login the id of the user to run the test as. + * @param permissions the permissions for the test. + */ + public static void login(String login, String... permissions) { + + Authentication authentication = new TestingAuthenticationToken(login, login, permissions); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + /** + * Logs off any previously logged on user. + */ + public static void logout() { + + SecurityContextHolder.getContext().setAuthentication(null); + } +} diff --git a/templates/server/src/main/resources/archetype-resources/__earProjectName__/pom.xml b/templates/server/src/main/resources/archetype-resources/__earProjectName__/pom.xml new file mode 100644 index 00000000..52f2bb3e --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/__earProjectName__/pom.xml @@ -0,0 +1,22 @@ + + 4.0.0 + + ${groupId} + ${artifactId} + ${version} + + ${earProjectName} + ${project.artifactId} + Enterprise application packaging. + ear + + + + ${groupId} + ${artifactId}-server + ${project.version} + war + + + \ No newline at end of file diff --git a/templates/server/src/main/resources/archetype-resources/api/pom.xml b/templates/server/src/main/resources/archetype-resources/api/pom.xml new file mode 100644 index 00000000..f7d1577f --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/api/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + ${groupId} + ${rootArtifactId} + ${version} + + ${rootArtifactId}-api + jar + ${project.artifactId} + API of the server for the ${rootArtifactId} application (containing datatypes, transfer-objects, and service interfaces). + + + + com.devonfw.java.modules + devon4j-rest + + + com.devonfw.java.modules + devon4j-logging + + + com.devonfw.java.modules + devon4j-security + + + + javax.servlet + javax.servlet-api + provided + + + diff --git a/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/ApplicationEntity.java b/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/ApplicationEntity.java new file mode 100644 index 00000000..28c6e165 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/ApplicationEntity.java @@ -0,0 +1,11 @@ +package ${package}.general.common.api; + +import net.sf.mmm.util.entity.api.MutableGenericEntity; + +/** + * This is the abstract interface for a {@link MutableGenericEntity} of this application. We are using {@link Long} for + * all {@link #getId() primary keys}. + */ +public abstract interface ApplicationEntity extends MutableGenericEntity { + +} diff --git a/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/BinaryObject.java b/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/BinaryObject.java new file mode 100644 index 00000000..24ae0193 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/BinaryObject.java @@ -0,0 +1,32 @@ +package ${package}.general.common.api; + +/** + * This is the interface for a {@link BinaryObject} of the ${rootArtifactId}. + */ +public interface BinaryObject extends ApplicationEntity { + + /** + * @param mimeType is the MIME-Type of this {@link BinaryObject} + */ + void setMimeType(String mimeType); + + /** + * Returns MIME-Type of thie {@link BinaryObject} + * + * @return the MIME-Type, e.g image/jpeg + */ + String getMimeType(); + + /** + * @return Returns the size in bytes + */ + long getSize(); + + /** + * Sets the size of bytes + * + * @param size the size in bytes + */ + void setSize(long size); + +} diff --git a/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/NlsBundleApplicationRoot.java b/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/NlsBundleApplicationRoot.java new file mode 100644 index 00000000..60aae37b --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/NlsBundleApplicationRoot.java @@ -0,0 +1,20 @@ +package ${package}.general.common.api; + +import net.sf.mmm.util.nls.api.NlsBundle; +import net.sf.mmm.util.nls.api.NlsBundleMessage; +import net.sf.mmm.util.nls.api.NlsMessage; + +/** + * This is the {@link NlsBundle} for this application. + */ +public interface NlsBundleApplicationRoot extends NlsBundle { + + /** + * @see ${package}.general.common.api.exception.NoActiveUserException + * + * @return the {@link NlsMessage}. + */ + @NlsBundleMessage("There is currently no user logged in") + NlsMessage errorNoActiveUser(); + +} diff --git a/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/UserProfile.java b/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/UserProfile.java new file mode 100644 index 00000000..9e2891b7 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/UserProfile.java @@ -0,0 +1,13 @@ +package ${package}.general.common.api; + +/** + * This is the interface for the profile of a user interacting with this application. + */ +public interface UserProfile { + + /** + * @return the login of the user. + */ + String getLogin(); + +} diff --git a/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/exception/ApplicationBusinessException.java b/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/exception/ApplicationBusinessException.java new file mode 100644 index 00000000..03b01bad --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/exception/ApplicationBusinessException.java @@ -0,0 +1,35 @@ +package ${package}.general.common.api.exception; + +import net.sf.mmm.util.nls.api.NlsMessage; + +/** + * Abstract base class for business exceptions of this application. + */ +public abstract class ApplicationBusinessException extends ApplicationException { + + private static final long serialVersionUID = 1L; + + /** + * @param message the error {@link #getNlsMessage() message}. + */ + public ApplicationBusinessException(NlsMessage message) { + + super(message); + } + + /** + * @param cause the error {@link #getCause() cause}. + * @param message the error {@link #getNlsMessage() message}. + */ + public ApplicationBusinessException(Throwable cause, NlsMessage message) { + + super(cause, message); + } + + @Override + public boolean isTechnical() { + + return false; + } + +} diff --git a/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/exception/ApplicationException.java b/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/exception/ApplicationException.java new file mode 100644 index 00000000..b4a0a09d --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/exception/ApplicationException.java @@ -0,0 +1,30 @@ +package ${package}.general.common.api.exception; + +import net.sf.mmm.util.exception.api.NlsRuntimeException; +import net.sf.mmm.util.nls.api.NlsMessage; + +/** + * Abstract base class for custom exceptions of this application. + */ +public abstract class ApplicationException extends NlsRuntimeException { + + private static final long serialVersionUID = 1L; + + /** + * @param message the error {@link #getNlsMessage() message}. + */ + public ApplicationException(NlsMessage message) { + + super(message); + } + + /** + * @param cause the error {@link #getCause() cause}. + * @param message the error {@link #getNlsMessage() message}. + */ + public ApplicationException(Throwable cause, NlsMessage message) { + + super(cause, message); + } + +} diff --git a/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/exception/ApplicationTechnicalException.java b/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/exception/ApplicationTechnicalException.java new file mode 100644 index 00000000..2c7ae764 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/exception/ApplicationTechnicalException.java @@ -0,0 +1,29 @@ +package ${package}.general.common.api.exception; + +import net.sf.mmm.util.nls.api.NlsMessage; + +/** + * Abstract base class for technical custom exceptions of this application. + */ +public abstract class ApplicationTechnicalException extends ApplicationException { + + private static final long serialVersionUID = 1L; + + /** + * @param message the error {@link #getNlsMessage() message}. + */ + public ApplicationTechnicalException(NlsMessage message) { + + super(message); + } + + /** + * @param cause the error {@link #getCause() cause}. + * @param message the error {@link #getNlsMessage() message}. + */ + public ApplicationTechnicalException(Throwable cause, NlsMessage message) { + + super(cause, message); + } + +} diff --git a/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/exception/NoActiveUserException.java b/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/exception/NoActiveUserException.java new file mode 100644 index 00000000..4b6e5f5f --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/exception/NoActiveUserException.java @@ -0,0 +1,31 @@ +package ${package}.general.common.api.exception; + +import ${package}.general.common.api.NlsBundleApplicationRoot; + +/** + * Thrown when an operation is requested that requires a user to be logged in, but no such user exists. + */ +public class NoActiveUserException extends ApplicationBusinessException { + + /** UID for serialization. */ + private static final long serialVersionUID = 1L; + + /** + * The constructor. + */ + public NoActiveUserException() { + + this(null); + } + + /** + * The constructor. + * + * @param cause The root cause of this exception. + */ + public NoActiveUserException(Throwable cause) { + + super(cause, createBundle(NlsBundleApplicationRoot.class).errorNoActiveUser()); + } + +} diff --git a/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/to/UserProfileTo.java b/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/to/UserProfileTo.java new file mode 100644 index 00000000..e2805b45 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/common/api/to/UserProfileTo.java @@ -0,0 +1,37 @@ +package ${package}.general.common.api.to; + +import ${package}.general.common.api.UserProfile; +import com.devonfw.module.basic.common.api.to.AbstractTo; + +/** + * Implementation of {@link UserProfile} as {AbstractTo TO}. + */ +public class UserProfileTo extends AbstractTo implements UserProfile { + + private static final long serialVersionUID = 1L; + + private String login; + + /** + * The constructor. + */ + public UserProfileTo() { + + super(); + } + + @Override + public String getLogin() { + + return this.login; + } + + /** + * @param login the new {@link #getLogin() login}. + */ + public void setLogin(String login) { + + this.login = login; + } + +} diff --git a/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/logic/api/to/BinaryObjectEto.java b/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/logic/api/to/BinaryObjectEto.java new file mode 100644 index 00000000..18d56cd1 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/logic/api/to/BinaryObjectEto.java @@ -0,0 +1,50 @@ +package ${package}.general.logic.api.to; + +import ${package}.general.common.api.BinaryObject; +import com.devonfw.module.basic.common.api.to.AbstractEto; + +/** + * The {@link com.devonfw.module.basic.common.api.to.AbstractEto ETO} for a {@link BinaryObject}. + */ +public class BinaryObjectEto extends AbstractEto implements BinaryObject { + + private static final long serialVersionUID = 1L; + + private String mimeType; + + private long size; + + /** + * Constructor. + */ + public BinaryObjectEto() { + + super(); + } + + @Override + public void setMimeType(String mimeType) { + + this.mimeType = mimeType; + + } + + @Override + public String getMimeType() { + + return this.mimeType; + } + + @Override + public long getSize() { + + return this.size; + } + + @Override + public void setSize(long size) { + + this.size = size; + } + +} diff --git a/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/service/api/rest/SecurityRestService.java b/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/service/api/rest/SecurityRestService.java new file mode 100644 index 00000000..784c0cef --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/api/src/main/java/__packageInPathFormat__/general/service/api/rest/SecurityRestService.java @@ -0,0 +1,38 @@ +package ${package}.general.service.api.rest; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; + +import org.springframework.security.web.csrf.CsrfToken; + +import com.devonfw.module.rest.common.api.RestService; + +import ${package}.general.common.api.to.UserProfileTo; + +/** + * The security REST service provides access to the csrf token, the authenticated user's meta-data. Furthermore, it + * provides functionality to check permissions and roles of the authenticated user. + */ +@Path("/security/v1") +public interface SecurityRestService extends RestService { + + /** + * @param request {@link HttpServletRequest} to retrieve the current session from. + * @param response {@link HttpServletResponse} to send additional information. + * @return the Spring Security {@link CsrfToken} from the server session. + */ + @GET + @Path("/csrftoken/") + CsrfToken getCsrfToken(@Context HttpServletRequest request, @Context HttpServletResponse response); + + /** + * @return the {@link UserProfileTo} of the currently logged-in user. + */ + @GET + @Path("/currentuser/") + UserProfileTo getCurrentUser(); + +} diff --git a/templates/server/src/main/resources/archetype-resources/core/pom.xml b/templates/server/src/main/resources/archetype-resources/core/pom.xml new file mode 100644 index 00000000..a1d45d0a --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/pom.xml @@ -0,0 +1,270 @@ + + + 4.0.0 + + ${groupId} + ${rootArtifactId} + ${version} + + ${rootArtifactId}-core + jar + ${project.artifactId} + Core of the server for the ${rootArtifactId} application - a simple example using the Open Application Standard Platform for Java (devon4j). + + + + ${project.groupId} + ${rootArtifactId}-api + ${project.version} + + + + + com.devonfw.java.modules + devon4j-beanmapping + + + + + com.devonfw.java.modules + devon4j-security + + + + com.devonfw.java.modules + devon4j-web + + + + + com.devonfw.java.starters + devon4j-starter-cxf-client-rest + + + + com.devonfw.java.starters + devon4j-starter-cxf-client-ws + + + + + com.devonfw.java.starters + devon4j-starter-cxf-server-rest + + + + com.devonfw.java.starters + devon4j-starter-cxf-server-ws + + + + + com.devonfw.java.starters + devon4j-starter-spring-data-jpa + + + + + org.springframework.boot + spring-boot-starter-jdbc + + + + + org.hibernate.javax.persistence + hibernate-jpa-2.1-api + + + + + org.hibernate + hibernate-entitymanager + + + + + com.querydsl + querydsl-jpa + + + com.querydsl + querydsl-apt + provided + + + + + org.hibernate.validator + hibernate-validator + + + + + javax.servlet + javax.servlet-api + provided + + + + + javax.el + javax.el-api + + + + + org.springframework + spring-webmvc + + + + +#if ($dbType == 'h2') + com.h2database + h2 +#elseif ($dbType == 'hsqldb') + org.hsqldb + hsqldb + 2.4.0 +#elseif ($dbType == 'postgresql') + org.postgresql + postgresql + +#elseif ($dbType == 'mysql') + mysql + mysql-connector-java + 8.0.8-dmr +#elseif ($dbType == 'mariadb') + org.mariadb.jdbc + mariadb-java-client + 1.5.4 +#elseif ($dbType == 'hana') + com.sap.cloud.db.jdbc + ngdbc + 2.3.48 +#elseif ($dbType == 'oracle') + com.oracle.jdbc + ojdbc8 + 12.2.0.1 +#elseif ($dbType == 'mssql') + com.microsoft.sqlserver + mssql-jdbc + 6.4.0.jre8 +#else + $dbType + $dbType + TODO +#end + +#if ($dbType != 'h2') + + com.h2database + h2 + test + +#end + + + + org.flywaydb + flyway-core + + + + + org.apache.cxf + cxf-rt-rs-service-description + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.springframework + spring-aop + + + + + cglib + cglib + + + + + com.devonfw.java.modules + devon4j-test + test + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + org.springframework.boot + spring-boot-starter-validation + + + + + + org.skyscreamer + jsonassert + test + + + + + + embedded + + true + + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + + + + + src/main/resources + true + + + + + org.apache.maven.plugins + maven-jar-plugin + + + config/application.properties + + + + + + + \ No newline at end of file diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/SpringBootApp.java b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/SpringBootApp.java new file mode 100644 index 00000000..1641ed89 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/SpringBootApp.java @@ -0,0 +1,30 @@ +package ${package}; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; + +import com.devonfw.module.jpa.dataaccess.api.AdvancedRevisionEntity; +import com.devonfw.module.jpa.dataaccess.impl.data.GenericRepositoryFactoryBean; + +/** + * Main entry point of this {@link SpringBootApplication}. Simply run this class to start this app. + */ +@SpringBootApplication +@EntityScan(basePackages = { "${package}" }, basePackageClasses = { AdvancedRevisionEntity.class }) +@EnableJpaRepositories(repositoryFactoryBeanClass = GenericRepositoryFactoryBean.class) +@EnableGlobalMethodSecurity(jsr250Enabled = true) +public class SpringBootApp { + + /** + * Entry point for spring-boot based app + * + * @param args - arguments + */ + public static void main(String[] args) { + + SpringApplication.run(SpringBootApp.class, args); + } +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/common/api/constants/PermissionConstants.java b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/common/api/constants/PermissionConstants.java new file mode 100644 index 00000000..f33c9575 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/common/api/constants/PermissionConstants.java @@ -0,0 +1,11 @@ +package ${package}.general.common.api.constants; + +/** + * Contains constants for the keys of all + * {@link com.devonfw.module.security.common.api.accesscontrol.AccessControlPermission}s. + */ +public abstract class PermissionConstants { + + // put your permission names from access-control-schema.xml as constants here (or generate with cobigen) + +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/common/base/AbstractBeanMapperSupport.java b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/common/base/AbstractBeanMapperSupport.java new file mode 100644 index 00000000..c1667fbf --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/common/base/AbstractBeanMapperSupport.java @@ -0,0 +1,31 @@ +package ${package}.general.common.base; + +import com.devonfw.module.beanmapping.common.api.BeanMapper; + +import javax.inject.Inject; + +/** + * This abstract class provides {@link #getBeanMapper() access} to the {@link BeanMapper}. + */ +public abstract class AbstractBeanMapperSupport { + + private BeanMapper beanMapper; + + /** + * @param beanMapper is the {@link BeanMapper} to {@link Inject} + */ + @Inject + public void setBeanMapper(BeanMapper beanMapper) { + + this.beanMapper = beanMapper; + } + + /** + * @return the {@link BeanMapper} instance. + */ + protected BeanMapper getBeanMapper() { + + return this.beanMapper; + } + +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/common/impl/config/ApplicationObjectMapperFactory.java b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/common/impl/config/ApplicationObjectMapperFactory.java new file mode 100644 index 00000000..f71bcf72 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/common/impl/config/ApplicationObjectMapperFactory.java @@ -0,0 +1,24 @@ +package ${package}.general.common.impl.config; + +import javax.inject.Named; + +import org.springframework.security.web.csrf.CsrfToken; + +import com.devonfw.module.json.common.base.ObjectMapperFactory; + +/** + * The MappingFactory class to resolve polymorphic conflicts within the ${rootArtifactId} application. + */ +@Named("ApplicationObjectMapperFactory") +public class ApplicationObjectMapperFactory extends ObjectMapperFactory { + + /** + * The constructor. + */ + public ApplicationObjectMapperFactory() { + + super(); + // register polymorphic mapping here - see https://github.com/oasp-forge/oasp4j-wiki/wiki/guide-json#json-and-inheritance + getExtensionModule().addAbstractTypeMapping(CsrfToken.class, CsrfTokenImpl.class); + } +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/common/impl/config/BeansDozerConfig.java b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/common/impl/config/BeansDozerConfig.java new file mode 100644 index 00000000..f49c315a --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/common/impl/config/BeansDozerConfig.java @@ -0,0 +1,32 @@ +package ${package}.general.common.impl.config; + +import java.util.ArrayList; +import java.util.List; + +import org.dozer.DozerBeanMapper; +import org.dozer.Mapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +/** + * Java bean configuration for Dozer + */ +@Configuration +@ComponentScan(basePackages = { "com.devonfw.module.beanmapping" }) +public class BeansDozerConfig { + + private static final String DOZER_MAPPING_XML = "config/app/common/dozer-mapping.xml"; + + /** + * @return the {@link DozerBeanMapper}. + */ + @Bean + public Mapper getDozer() { + + List beanMappings = new ArrayList<>(); + beanMappings.add(DOZER_MAPPING_XML); + return new DozerBeanMapper(beanMappings); + + } +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/common/impl/config/CsrfTokenImpl.java b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/common/impl/config/CsrfTokenImpl.java new file mode 100644 index 00000000..3c59784f --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/common/impl/config/CsrfTokenImpl.java @@ -0,0 +1,60 @@ +package ${package}.general.common.impl.config; + +import org.springframework.security.web.csrf.CsrfToken; + +/** + * Implementation of {@link CsrfToken} as Java bean for JSON deserialization. + */ +public class CsrfTokenImpl implements CsrfToken { + + private static final long serialVersionUID = 1L; + + private String headerName; + + private String parameterName; + + private String token; + + @Override + public String getHeaderName() { + + return this.headerName; + } + + @Override + public String getParameterName() { + + return this.parameterName; + } + + @Override + public String getToken() { + + return this.token; + } + + /** + * @param headerName new value of {@link #getHeaderName()}. + */ + public void setHeaderName(String headerName) { + + this.headerName = headerName; + } + + /** + * @param parameterName new value of {@link #getParameterName()}. + */ + public void setParameterName(String parameterName) { + + this.parameterName = parameterName; + } + + /** + * @param token new value of {@link #getToken()}. + */ + public void setToken(String token) { + + this.token = token; + } + +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/common/impl/security/BaseUserDetailsService.java b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/common/impl/security/BaseUserDetailsService.java new file mode 100644 index 00000000..0da061d0 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/common/impl/security/BaseUserDetailsService.java @@ -0,0 +1,123 @@ +package ${package}.general.common.impl.security; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import javax.inject.Inject; +import javax.inject.Named; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +import com.devonfw.module.security.common.api.accesscontrol.AccessControl; +import com.devonfw.module.security.common.api.accesscontrol.AccessControlProvider; +import com.devonfw.module.security.common.base.accesscontrol.AccessControlGrantedAuthority; + +/** + * Custom implementation of {@link UserDetailsService}.
+ * + * @see ${package}.general.service.impl.config.BaseWebSecurityConfig + */ +@Named +public class BaseUserDetailsService implements UserDetailsService { + + /** Logger instance. */ + private static final Logger LOG = LoggerFactory.getLogger(BaseUserDetailsService.class); + + private AuthenticationManagerBuilder amBuilder; + + private AccessControlProvider accessControlProvider; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + + Set authorities = getAuthorities(username); + UserDetails user; + try { + user = getAmBuilder().getDefaultUserDetailsService().loadUserByUsername(username); + User userData = new User(user.getUsername(), user.getPassword(), authorities); + return userData; + } catch (Exception e) { + e.printStackTrace(); + UsernameNotFoundException exception = new UsernameNotFoundException("Authentication failed.", e); + LOG.warn("Failed to get user {}.", username, exception); + throw exception; + } + } + + /** + * @param username the login of the user + * @return the associated {@link GrantedAuthority}s + * @throws AuthenticationException if no principal is retrievable for the given {@code username} + */ + protected Set getAuthorities(String username) throws AuthenticationException { + + Objects.requireNonNull(username, "username"); + // determine granted authorities for spring-security... + Set authorities = new HashSet<>(); + Collection accessControlIds = getRoles(username); + Set accessControlSet = new HashSet<>(); + for (String id : accessControlIds) { + boolean success = this.accessControlProvider.collectAccessControls(id, accessControlSet); + if (!success) { + LOG.warn("Undefined access control {}.", id); + } + } + for (AccessControl accessControl : accessControlSet) { + authorities.add(new AccessControlGrantedAuthority(accessControl)); + } + return authorities; + } + + private Collection getRoles(String username) { + + Collection roles = new ArrayList<>(); + // TODO for a reasonable application you need to retrieve the roles of the user from a central IAM system + roles.add(username); + return roles; + } + + /** + * @return amBuilder + */ + public AuthenticationManagerBuilder getAmBuilder() { + + return this.amBuilder; + } + + /** + * @param amBuilder new value of {@link #getAmBuilder}. + */ + @Inject + public void setAmBuilder(AuthenticationManagerBuilder amBuilder) { + + this.amBuilder = amBuilder; + } + + /** + * @return accessControlProvider + */ + public AccessControlProvider getAccessControlProvider() { + + return this.accessControlProvider; + } + + /** + * @param accessControlProvider new value of {@link #getAccessControlProvider}. + */ + @Inject + public void setAccessControlProvider(AccessControlProvider accessControlProvider) { + + this.accessControlProvider = accessControlProvider; + } +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/common/impl/security/CsrfRequestMatcher.java b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/common/impl/security/CsrfRequestMatcher.java new file mode 100644 index 00000000..4fd100ec --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/common/impl/security/CsrfRequestMatcher.java @@ -0,0 +1,54 @@ +package ${package}.general.common.impl.security; + +import java.util.regex.Pattern; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.web.util.matcher.RequestMatcher; + +/** + * This is the implementation of {@link RequestMatcher}, which decides which {@link HttpServletRequest Requests} require + * a correct CSRF token. + * + * @see Cross-site request forgery + */ +public class CsrfRequestMatcher implements RequestMatcher { + + private static final Pattern HTTP_METHOD_PATTERN = Pattern.compile("^GET$"); + + private static final String[] PATH_PREFIXES_WITHOUT_CSRF_PROTECTION = + { "/login", "/logout", "/services/rest/login", "/websocket" }; + + @Override + public boolean matches(HttpServletRequest request) { + + // GET requests are read-only and therefore do not need CSRF protection + if (HTTP_METHOD_PATTERN.matcher(request.getMethod()).matches()) { + + return false; + } + + // There are specific URLs which can not be protected from CSRF. For example, in case of the the login page, + // the CSRF token can only be accessed after a successful authentication ("login"). + String requestPath = getRequestPath(request); + for (String path : PATH_PREFIXES_WITHOUT_CSRF_PROTECTION) { + if (requestPath.startsWith(path)) { + return false; + } + } + + return true; + } + + private String getRequestPath(HttpServletRequest request) { + + String url = request.getServletPath(); + String pathInfo = request.getPathInfo(); + + if (pathInfo != null) { + url += pathInfo; + } + + return url; + } +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/dataaccess/api/ApplicationPersistenceEntity.java b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/dataaccess/api/ApplicationPersistenceEntity.java new file mode 100644 index 00000000..31abc2bf --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/dataaccess/api/ApplicationPersistenceEntity.java @@ -0,0 +1,104 @@ +package ${package}.general.dataaccess.api; + +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.MappedSuperclass; +import javax.persistence.Transient; +import javax.persistence.Version; + +import ${package}.general.common.api.ApplicationEntity; +import com.devonfw.module.jpa.dataaccess.api.MutablePersistenceEntity; + +/** + * Abstract Entity for all Entities with an id and a version field. + */ +@MappedSuperclass +public abstract class ApplicationPersistenceEntity implements ApplicationEntity, MutablePersistenceEntity { + + private static final long serialVersionUID = 1L; + + /** @see #getId() */ + private Long id; + + /** @see #getModificationCounter() */ + private int modificationCounter; + + /** @see #getRevision() */ + private Number revision; + + /** + * The constructor. + */ + public ApplicationPersistenceEntity() { + + super(); + } + + @Override + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + public Long getId() { + + return this.id; + } + + @Override + public void setId(Long id) { + + this.id = id; + } + + @Override + @Version + public int getModificationCounter() { + + return this.modificationCounter; + } + + @Override + public void setModificationCounter(int version) { + + this.modificationCounter = version; + } + + @Override + @Transient + public Number getRevision() { + + return this.revision; + } + + /** + * @param revision the revision to set + */ + @Override + public void setRevision(Number revision) { + + this.revision = revision; + } + + @Override + public String toString() { + + StringBuilder buffer = new StringBuilder(); + toString(buffer); + return buffer.toString(); + } + + /** + * Method to extend {@link #toString()} logic. + * + * @param buffer is the {@link StringBuilder} where to {@link StringBuilder#append(Object) append} the string + * representation. + */ + protected void toString(StringBuilder buffer) { + + buffer.append(getClass().getSimpleName()); + if (this.id != null) { + buffer.append("[id="); + buffer.append(this.id); + buffer.append("]"); + } + } +} \ No newline at end of file diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/dataaccess/api/BinaryObjectEntity.java b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/dataaccess/api/BinaryObjectEntity.java new file mode 100644 index 00000000..c1fb02c4 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/dataaccess/api/BinaryObjectEntity.java @@ -0,0 +1,86 @@ +package ${package}.general.dataaccess.api; + +import java.sql.Blob; + +import javax.persistence.Entity; +#if ($dbType == 'postgresql') +import org.hibernate.annotations.Type; +#else +import javax.persistence.Lob; +#end +import javax.persistence.Table; +import javax.persistence.Column; + +import ${package}.general.common.api.BinaryObject; + +/** + * {@link ApplicationPersistenceEntity Entity} for {@link BinaryObject}. Contains the actual {@link Blob}. + */ +@Entity +@Table(name = "BinaryObject") +public class BinaryObjectEntity extends ApplicationPersistenceEntity implements BinaryObject { + + private static final long serialVersionUID = 1L; + + private Blob data; + + private String mimeType; + + private long size; + + /** + * The constructor. + */ + public BinaryObjectEntity() { + + super(); + } + + @Override + public void setMimeType(String mimeType) { + + this.mimeType = mimeType; + + } + + @Override + public String getMimeType() { + + return this.mimeType; + } + + /** + * @return the {@link Blob} data. + */ +#if ($dbType == 'postgresql') + @Type(type = "org.hibernate.type.BinaryType") +#else + @Lob +#end + @Column(name = "content") + public Blob getData() { + + return this.data; + } + + /** + * @param data the data to set + */ + public void setData(Blob data) { + + this.data = data; + } + + @Column(name = "filesize") + @Override + public long getSize() { + + return this.size; + } + + @Override + public void setSize(long size) { + + this.size = size; + } +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/dataaccess/api/dao/BinaryObjectRepository.java b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/dataaccess/api/dao/BinaryObjectRepository.java new file mode 100644 index 00000000..3a8987e0 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/dataaccess/api/dao/BinaryObjectRepository.java @@ -0,0 +1,11 @@ +package ${package}.general.dataaccess.api.dao; + +import com.devonfw.module.jpa.dataaccess.api.data.DefaultRepository; +import ${package}.general.dataaccess.api.BinaryObjectEntity; + +/** + * {@link DefaultRepository} for {@link BinaryObjectEntity}. + */ +public interface BinaryObjectRepository extends DefaultRepository { + +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/logic/api/UcManageBinaryObject.java b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/logic/api/UcManageBinaryObject.java new file mode 100644 index 00000000..5b67285c --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/logic/api/UcManageBinaryObject.java @@ -0,0 +1,37 @@ +package ${package}.general.logic.api; + +import ${package}.general.logic.api.to.BinaryObjectEto; + +import java.sql.Blob; + +/** + * Use case for managing BinaryObject. + * + */ +public interface UcManageBinaryObject { + + /** + * @param data the Blob data to save + * @param binaryObjectEto the {@link BinaryObjectEto} + * @return {@link BinaryObjectEto} + */ + BinaryObjectEto saveBinaryObject(Blob data, BinaryObjectEto binaryObjectEto); + + /** + * @param binaryObjectId the ID of the {@link BinaryObjectEto} that should be deleted + */ + void deleteBinaryObject(Long binaryObjectId); + + /** + * @param binaryObjectId the ID of the {@link BinaryObjectEto} to find + * @return {@link BinaryObjectEto} + */ + BinaryObjectEto findBinaryObject(Long binaryObjectId); + + /** + * @param binaryObjectId the ID of the {@link BinaryObjectEto} the blob corresponds to + * @return {@link Blob} + */ + Blob getBinaryObjectBlob(Long binaryObjectId); + +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/logic/base/AbstractComponentFacade.java b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/logic/base/AbstractComponentFacade.java new file mode 100644 index 00000000..0b2a0011 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/logic/base/AbstractComponentFacade.java @@ -0,0 +1,16 @@ +package ${package}.general.logic.base; + +/** + * Abstract base class for any component implementation class in this application. + */ +public abstract class AbstractComponentFacade extends AbstractLogic { + + /** + * The constructor. + */ + public AbstractComponentFacade() { + + super(); + } + +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/logic/base/AbstractLogic.java b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/logic/base/AbstractLogic.java new file mode 100644 index 00000000..9e333de5 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/logic/base/AbstractLogic.java @@ -0,0 +1,84 @@ +package ${package}.general.logic.base; + +import ${package}.general.common.base.AbstractBeanMapperSupport; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import net.sf.mmm.util.entity.api.GenericEntity; +import net.sf.mmm.util.entity.api.PersistenceEntity; +import net.sf.mmm.util.transferobject.api.AbstractTransferObject; +import net.sf.mmm.util.transferobject.api.TransferObject; + +/** + * Abstract base class for implementations of business logic in this application. Actual implementations need + * to be annotated with {@link javax.inject.Named}. + * + * @see AbstractUc + * @see AbstractComponentFacade + */ +public abstract class AbstractLogic extends AbstractBeanMapperSupport { + + /** + * The constructor. + */ + public AbstractLogic() { + + super(); + } + + /** + * Creates a {@link Map} with all {@link GenericEntity entities} from the given {@link Collection} using their + * {@link GenericEntity#getId() ID} as key. All {@link GenericEntity entities} without an + * {@link GenericEntity#getId() ID} ({@code null}) will be ignored. + * + * @param is the generic type of the {@link GenericEntity#getId() ID}. + * @param is the generic type of the {@link GenericEntity entity}. + * @param entities is the {@link Collection} of {@link GenericEntity entities}. + * @return a {@link Map} mapping from {@link GenericEntity#getId() ID} to {@link GenericEntity entity}. + */ + protected static > Map getEntityMap(Collection entities) { + + Map id2EntityMap = new HashMap<>(); + for (E entity : entities) { + ID id = entity.getId(); + if (id != null) { + id2EntityMap.put(id, entity); + } + } + return id2EntityMap; + } + + /** + * Determines the {@link GenericEntity entities} to delete if currentList is the current list from the + * persistence and listToSave is the new list that shall be saved. In other words this method selects the + * {@link GenericEntity entities} from currentList that are not contained in listToSave. + * + * @param is the generic type of the {@link GenericEntity#getId() ID}. + * @param is the generic type of the {@link GenericEntity entity}. + * @param currentEntities is the {@link Collection} of the {@link GenericEntity entities} currently persisted. We + * assume that all objects in this list have an {@link GenericEntity#getId() ID} value (that is not + * {@code null}). + * @param entitiesToSave is the {@link Collection} that contains the {@link GenericEntity entities} that shall be + * saved. It may contain {@link GenericEntity entities} that have no {@link GenericEntity#getId() ID} that + * shall be newly created. + * @return the {@link List} with the {@link GenericEntity entities} to delete. + */ + protected static > List getEntities2Delete(Collection currentEntities, + Collection entitiesToSave) { + + List result = new ArrayList<>(currentEntities.size()); + Map entityMap = getEntityMap(entitiesToSave); + for (E entity : currentEntities) { + if (!entityMap.containsKey(entity.getId())) { + // entity from currentList is not contained in listToSave... + result.add(entity); + } + } + return result; + } + +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/logic/base/AbstractUc.java b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/logic/base/AbstractUc.java new file mode 100644 index 00000000..167ffddd --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/logic/base/AbstractUc.java @@ -0,0 +1,18 @@ +package ${package}.general.logic.base; + +/** + * Abstract base class for any use case in this application. Actual implementations need to be annotated with + * {@link javax.inject.Named}. + * + */ +public abstract class AbstractUc extends AbstractLogic { + + /** + * The constructor. + */ + public AbstractUc() { + + super(); + } + +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/logic/impl/UcManageBinaryObjectImpl.java b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/logic/impl/UcManageBinaryObjectImpl.java new file mode 100644 index 00000000..b1ea3ffe --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/logic/impl/UcManageBinaryObjectImpl.java @@ -0,0 +1,67 @@ +package ${package}.general.logic.impl; + +import java.sql.Blob; + +import javax.inject.Inject; +import javax.inject.Named; + +import ${package}.general.dataaccess.api.BinaryObjectEntity; +import ${package}.general.dataaccess.api.dao.BinaryObjectRepository; +import ${package}.general.logic.api.UcManageBinaryObject; +import ${package}.general.logic.api.to.BinaryObjectEto; +import ${package}.general.logic.base.AbstractUc; + +/** + * Implementation of {@link UcManageBinaryObject}. + */ +@Named +public class UcManageBinaryObjectImpl extends AbstractUc implements UcManageBinaryObject { + + private BinaryObjectRepository binaryObjectRepository; + + /** + * @return {@link BinaryObjectRepository} instance. + */ + public BinaryObjectRepository getBinaryObjectRepository() { + + return this.binaryObjectRepository; + } + + /** + * @param binaryObjectRepository the {@link BinaryObjectRepository} to set + */ + @Inject + public void setBinaryObjectRepository(BinaryObjectRepository binaryObjectRepository) { + + this.binaryObjectRepository = binaryObjectRepository; + } + + @Override + public BinaryObjectEto saveBinaryObject(Blob data, BinaryObjectEto binaryObjectEto) { + + BinaryObjectEntity binaryObjectEntity = getBeanMapper().map(binaryObjectEto, BinaryObjectEntity.class); + binaryObjectEntity.setData(data); + this.binaryObjectRepository.save(binaryObjectEntity); + return getBeanMapper().map(binaryObjectEntity, BinaryObjectEto.class); + } + + @Override + public void deleteBinaryObject(Long binaryObjectId) { + + this.binaryObjectRepository.deleteById(binaryObjectId); + + } + + @Override + public BinaryObjectEto findBinaryObject(Long binaryObjectId) { + + return getBeanMapper().map(this.binaryObjectRepository.find(binaryObjectId), BinaryObjectEto.class); + } + + @Override + public Blob getBinaryObjectBlob(Long binaryObjectId) { + + return this.binaryObjectRepository.find(binaryObjectId).getData(); + } + +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/logic/impl/config/DefaultRolesPrefixPostProcessor.java b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/logic/impl/config/DefaultRolesPrefixPostProcessor.java new file mode 100644 index 00000000..f7fbf5f7 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/logic/impl/config/DefaultRolesPrefixPostProcessor.java @@ -0,0 +1,61 @@ +package ${package}.general.logic.impl.config; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.core.PriorityOrdered; +import org.springframework.security.access.annotation.Jsr250MethodSecurityMetadataSource; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler; +import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; + +/** + * This is an implementation of {@link BeanPostProcessor} that allows to change the role prefix of spring-security. By + * default spring-security is magically adding a strange prefix called "ROLE_" to your granted authorities. In order to + * prevent this we use this class with an empty prefix. + */ +public class DefaultRolesPrefixPostProcessor implements BeanPostProcessor, PriorityOrdered { + + private final String rolePrefix; + + /** + * Der Konstruktor. + * + * @param rolePrefix das gewünschte Rollen-Präfix (z.B. der leere String). + */ + public DefaultRolesPrefixPostProcessor(String rolePrefix) { + super(); + this.rolePrefix = rolePrefix; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + + // remove this if you are not using JSR-250 + if (bean instanceof Jsr250MethodSecurityMetadataSource) { + ((Jsr250MethodSecurityMetadataSource) bean).setDefaultRolePrefix(this.rolePrefix); + } + + if (bean instanceof DefaultMethodSecurityExpressionHandler) { + ((DefaultMethodSecurityExpressionHandler) bean).setDefaultRolePrefix(this.rolePrefix); + } + if (bean instanceof DefaultWebSecurityExpressionHandler) { + ((DefaultWebSecurityExpressionHandler) bean).setDefaultRolePrefix(this.rolePrefix); + } + if (bean instanceof SecurityContextHolderAwareRequestFilter) { + ((SecurityContextHolderAwareRequestFilter) bean).setRolePrefix(this.rolePrefix); + } + return bean; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + + return bean; + } + + @Override + public int getOrder() { + + return PriorityOrdered.HIGHEST_PRECEDENCE; + } +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/service/impl/LoginController.java b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/service/impl/LoginController.java new file mode 100644 index 00000000..9fcc607c --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/service/impl/LoginController.java @@ -0,0 +1,57 @@ +package ${package}.general.service.impl; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.ModelAndView; + +/** + * Controller for Login-Page. + */ +@Controller +public class LoginController { + + /** + * Default URL to redirect to after successfully login. + */ + public final static String defaultTargetUrl = "/"; + + /** + * Builds the model for the login page---mainly focusing on the error message handling. + * + * @param authentication_failed flag for authentication failed + * @param invalid_session flag for invalid session + * @param access_denied flag for access denied + * @param logout flag for successful logout + * @return the view model + */ + @RequestMapping(value = "/login**", method = {RequestMethod.GET,RequestMethod.POST}) + public ModelAndView login( + @RequestParam(value = "authentication_failed", required = false) boolean authentication_failed, + @RequestParam(value = "invalid_session", required = false) boolean invalid_session, + @RequestParam(value = "access_denied", required = false) boolean access_denied, + @RequestParam(value = "logout", required = false) boolean logout) { + + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (!authentication.getPrincipal().equals("anonymousUser")) { + return new ModelAndView("redirect:" + defaultTargetUrl); + } + + ModelAndView model = new ModelAndView(); + if (authentication_failed) { + model.addObject("error", "Authentication failed!"); + } else if (invalid_session) { + model.addObject("error", "You are currently not logged in!"); + } else if (access_denied) { + model.addObject("error", "You have insufficient permissions to access this page!"); + } else if (logout) { + model.addObject("msg", "Logout successful!"); + } + + return model; + } + +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/service/impl/config/BaseWebSecurityConfig.java b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/service/impl/config/BaseWebSecurityConfig.java new file mode 100644 index 00000000..22e11fed --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/service/impl/config/BaseWebSecurityConfig.java @@ -0,0 +1,146 @@ +package ${package}.general.service.impl.config; + +import javax.inject.Inject; +import javax.servlet.Filter; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.csrf.CsrfFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import ${package}.general.common.impl.security.CsrfRequestMatcher; +import com.devonfw.module.security.common.impl.rest.AuthenticationSuccessHandlerSendingOkHttpStatusCode; +import com.devonfw.module.security.common.impl.rest.JsonUsernamePasswordAuthenticationFilter; +import com.devonfw.module.security.common.impl.rest.LogoutSuccessHandlerReturningOkHttpStatusCode; + +/** + * This type serves as a base class for extensions of the {@code WebSecurityConfigurerAdapter} and provides a default + * configuration.
+ * Security configuration is based on {@link WebSecurityConfigurerAdapter}. This configuration is by purpose designed + * most simple for two channels of authentication: simple login form and rest-url. + */ +public abstract class BaseWebSecurityConfig extends WebSecurityConfigurerAdapter { + + @Value("${security.cors.enabled}") + boolean corsEnabled = false; + + @Inject + private UserDetailsService userDetailsService; + + @Inject + private PasswordEncoder passwordEncoder; + + private CorsFilter getCorsFilter() { + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOrigin("*"); + config.addAllowedHeader("*"); + config.addAllowedMethod("OPTIONS"); + config.addAllowedMethod("HEAD"); + config.addAllowedMethod("GET"); + config.addAllowedMethod("PUT"); + config.addAllowedMethod("POST"); + config.addAllowedMethod("DELETE"); + config.addAllowedMethod("PATCH"); + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } + + /** + * Configure spring security to enable a simple webform-login + a simple rest login. + */ + @Override + public void configure(HttpSecurity http) throws Exception { + + String[] unsecuredResources = new String[] { "/login", "/security/**", "/services/rest/login", + "/services/rest/logout" }; + + http + // + .userDetailsService(this.userDetailsService) + // define all urls that are not to be secured + .authorizeRequests().antMatchers(unsecuredResources).permitAll().anyRequest().authenticated().and() + + // activate crsf check for a selection of urls (but not for login & logout) + .csrf().requireCsrfProtectionMatcher(new CsrfRequestMatcher()).and() + + // configure parameters for simple form login (and logout) + .formLogin().successHandler(new SimpleUrlAuthenticationSuccessHandler()).defaultSuccessUrl("/") + .failureUrl("/login.html?error").loginProcessingUrl("/j_spring_security_login").usernameParameter("username") + .passwordParameter("password").and() + // logout via POST is possible + .logout().logoutSuccessUrl("/login.html").and() + + // register login and logout filter that handles rest logins + .addFilterAfter(getSimpleRestAuthenticationFilter(), BasicAuthenticationFilter.class) + .addFilterAfter(getSimpleRestLogoutFilter(), LogoutFilter.class); + + if (this.corsEnabled) { + http.addFilterBefore(getCorsFilter(), CsrfFilter.class); + } + } + + /** + * Create a simple filter that allows logout on a REST Url /services/rest/logout and returns a simple HTTP status 200 + * ok. + * + * @return the filter. + */ + protected Filter getSimpleRestLogoutFilter() { + + LogoutFilter logoutFilter = new LogoutFilter(new LogoutSuccessHandlerReturningOkHttpStatusCode(), + new SecurityContextLogoutHandler()); + + // configure logout for rest logouts + logoutFilter.setLogoutRequestMatcher(new AntPathRequestMatcher("/services/rest/logout")); + + return logoutFilter; + } + + /** + * Create a simple authentication filter for REST logins that reads user-credentials from a json-parameter and returns + * status 200 instead of redirect after login. + * + * @return the {@link JsonUsernamePasswordAuthenticationFilter}. + * @throws Exception if something goes wrong. + */ + protected JsonUsernamePasswordAuthenticationFilter getSimpleRestAuthenticationFilter() throws Exception { + + JsonUsernamePasswordAuthenticationFilter jsonFilter = new JsonUsernamePasswordAuthenticationFilter( + new AntPathRequestMatcher("/services/rest/login")); + jsonFilter.setPasswordParameter("j_password"); + jsonFilter.setUsernameParameter("j_username"); + jsonFilter.setAuthenticationManager(authenticationManager()); + // set failurehandler that uses no redirect in case of login failure; just HTTP-status: 401 + jsonFilter.setAuthenticationManager(authenticationManagerBean()); + jsonFilter.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler()); + // set successhandler that uses no redirect in case of login success; just HTTP-status: 200 + jsonFilter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandlerSendingOkHttpStatusCode()); + return jsonFilter; + } + + @SuppressWarnings("javadoc") + @Inject + public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + + auth.inMemoryAuthentication().withUser("waiter").password(this.passwordEncoder.encode("waiter")).roles("Waiter") + .and().withUser("cook").password(this.passwordEncoder.encode("cook")).roles("Cook").and().withUser("barkeeper") + .password(this.passwordEncoder.encode("barkeeper")).roles("Barkeeper").and().withUser("chief") + .password(this.passwordEncoder.encode("chief")).roles("Chief"); + } + +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/service/impl/config/ServletInitializer.java b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/service/impl/config/ServletInitializer.java new file mode 100644 index 00000000..f26cc174 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/service/impl/config/ServletInitializer.java @@ -0,0 +1,23 @@ +package ${package}.general.service.impl.config; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.context.annotation.Configuration; + +import ${package}.SpringBootApp; + +/** + * This auto configuration will be used by spring boot to enable traditional deployment to a servlet container. You may + * remove this class if you run your application with embedded tomcat only. Tomcat startup will be twice as fast. + */ +@Configuration +@EnableAutoConfiguration +public class ServletInitializer extends SpringBootServletInitializer { + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + + return application.sources(SpringBootApp.class); + } +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/service/impl/config/WebConfig.java b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/service/impl/config/WebConfig.java new file mode 100644 index 00000000..1ef96539 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/service/impl/config/WebConfig.java @@ -0,0 +1,80 @@ +package ${package}.general.service.impl.config; + +import javax.servlet.Filter; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.CharacterEncodingFilter; + +import com.devonfw.module.logging.common.api.DiagnosticContextFacade; +import com.devonfw.module.logging.common.impl.DiagnosticContextFacadeImpl; +import com.devonfw.module.logging.common.impl.DiagnosticContextFilter; +import com.devonfw.module.logging.common.impl.PerformanceLogFilter; +import com.devonfw.module.service.common.api.constants.ServiceConstants; + +/** + * Registers a number of filters for web requests. + */ +@Configuration +public class WebConfig { + + private @Autowired AutowireCapableBeanFactory beanFactory; + + /** + * @return the {@link FilterRegistrationBean} to register the {@link PerformanceLogFilter} that will log all requests + * with their duration and status code. + */ + @Bean + public FilterRegistrationBean performanceLogFilter() { + + FilterRegistrationBean registration = new FilterRegistrationBean(); + Filter performanceLogFilter = new PerformanceLogFilter(); + this.beanFactory.autowireBean(performanceLogFilter); + registration.setFilter(performanceLogFilter); + registration.addUrlPatterns("/*"); + return registration; + } + + /** + * @return the {@link DiagnosticContextFacade} implementation. + */ + @Bean(name = "DiagnosticContextFacade") + public DiagnosticContextFacade diagnosticContextFacade() { + + return new DiagnosticContextFacadeImpl(); + } + + /** + * @return the {@link FilterRegistrationBean} to register the {@link DiagnosticContextFilter} that adds the + * correlation id as MDC so it will be included in all associated logs. + */ + @Bean + public FilterRegistrationBean diagnosticContextFilter() { + + FilterRegistrationBean registration = new FilterRegistrationBean(); + Filter diagnosticContextFilter = new DiagnosticContextFilter(); + this.beanFactory.autowireBean(diagnosticContextFilter); + registration.setFilter(diagnosticContextFilter); + registration.addUrlPatterns(ServiceConstants.URL_PATH_SERVICES + "/*"); + return registration; + } + + /** + * @return the {@link FilterRegistrationBean} to register the {@link CharacterEncodingFilter} to set the encoding. + */ + @Bean + public FilterRegistrationBean setCharacterEncodingFilter() { + + FilterRegistrationBean registration = new FilterRegistrationBean(); + CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter(); + characterEncodingFilter.setEncoding("UTF-8"); + characterEncodingFilter.setForceEncoding(false); + this.beanFactory.autowireBean(characterEncodingFilter); + registration.setFilter(characterEncodingFilter); + registration.addUrlPatterns("/*"); + return registration; + } +} \ No newline at end of file diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/service/impl/config/WebSecurityBeansConfig.java b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/service/impl/config/WebSecurityBeansConfig.java new file mode 100644 index 00000000..f98771ff --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/service/impl/config/WebSecurityBeansConfig.java @@ -0,0 +1,79 @@ +package ${package}.general.service.impl.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.csrf.CsrfTokenRepository; +import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository; + +import ${package}.general.logic.impl.config.DefaultRolesPrefixPostProcessor; +import com.devonfw.module.security.common.api.accesscontrol.AccessControlProvider; +import com.devonfw.module.security.common.base.accesscontrol.AccessControlSchemaProvider; +import com.devonfw.module.security.common.impl.accesscontrol.AccessControlProviderImpl; +import com.devonfw.module.security.common.impl.accesscontrol.AccessControlSchemaProviderImpl; + +/** + * This configuration class provides factory methods for several Spring security related beans. + * + */ +@Configuration +public class WebSecurityBeansConfig { + + /** + * This method provides a new instance of {@code AccessControlProvider} + * + * @return the newly created {@code AccessControlProvider} + */ + @Bean + public AccessControlProvider accessControlProvider() { + + return new AccessControlProviderImpl(); + } + + /** + * This method provides a new instance of {@code AccessControlSchemaProvider} + * + * @return the newly created {@code AccessControlSchemaProvider} + */ + @Bean + public AccessControlSchemaProvider accessControlSchemaProvider() { + + return new AccessControlSchemaProviderImpl(); + } + + /** + * This method provides a new instance of {@code CsrfTokenRepository} + * + * @return the newly created {@code CsrfTokenRepository} + */ + @Bean + public CsrfTokenRepository csrfTokenRepository() { + + return new HttpSessionCsrfTokenRepository(); + } + + /** + * This method provides a new instance of {@code DefaultRolesPrefixPostProcessor} + * + * @return the newly create {@code DefaultRolesPrefixPostProcessor} + */ + @Bean + public static DefaultRolesPrefixPostProcessor defaultRolesPrefixPostProcessor() { + + // By default Spring-Security is setting the prefix "ROLE_" for all permissions/authorities. + // We disable this undesired behavior here... + return new DefaultRolesPrefixPostProcessor(""); + } + + /** + * This method provide a new instance of {@code DelegatingPasswordEncoder} + * + * @return the newly create {@code DelegatingPasswordEncoder} + */ + @Bean + public PasswordEncoder passwordEncoder() { + + return PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/service/impl/config/WebSecurityConfig.java b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/service/impl/config/WebSecurityConfig.java new file mode 100644 index 00000000..685eccfe --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/service/impl/config/WebSecurityConfig.java @@ -0,0 +1,21 @@ +package ${package}.general.service.impl.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +import com.devonfw.module.basic.common.api.config.SpringProfileConstants; + +/** + * Security configuration based on {@link WebSecurityConfigurerAdapter}. This configuration is by purpose designed most + * simple for two channels of authentication: simple login form and rest-url. (Copied from + * {@link ${package}.general.service.impl.config.BaseWebSecurityConfig} + * + */ +@Configuration +@EnableWebSecurity +@Profile(SpringProfileConstants.NOT_JUNIT) +public class WebSecurityConfig extends BaseWebSecurityConfig { + +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/service/impl/rest/ApplicationAccessDeniedHandler.java b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/service/impl/rest/ApplicationAccessDeniedHandler.java new file mode 100644 index 00000000..6fddd558 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/service/impl/rest/ApplicationAccessDeniedHandler.java @@ -0,0 +1,46 @@ +package ${package}.general.service.impl.rest; + +import com.devonfw.module.rest.service.impl.RestServiceExceptionFacade; + +import java.io.IOException; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.Response; + +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; + +/** + * + */ +@Named("ApplicationAccessDeniedHandler") +public class ApplicationAccessDeniedHandler implements AccessDeniedHandler { + + private RestServiceExceptionFacade exceptionFacade; + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException, ServletException { + + Response restResponse = this.exceptionFacade.toResponse(accessDeniedException); + Object entity = restResponse.getEntity(); + response.setStatus(restResponse.getStatus()); + if (entity != null) { + response.getWriter().write(entity.toString()); + } + } + + /** + * @param exceptionFacade the exceptionFacade to set + */ + @Inject + public void setExceptionFacade(RestServiceExceptionFacade exceptionFacade) { + + this.exceptionFacade = exceptionFacade; + } + +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/service/impl/rest/SecurityRestServiceImpl.java b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/service/impl/rest/SecurityRestServiceImpl.java new file mode 100644 index 00000000..bb6a49a8 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/java/__packageInPathFormat__/general/service/impl/rest/SecurityRestServiceImpl.java @@ -0,0 +1,77 @@ +package ${package}.general.service.impl.rest; + +import javax.annotation.security.PermitAll; +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.transaction.Transactional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.security.web.csrf.CsrfTokenRepository; + +import ${package}.general.common.api.exception.NoActiveUserException; +import ${package}.general.common.api.to.UserProfileTo; +import ${package}.general.service.api.rest.SecurityRestService; + +/** + * Implementation of {@link SecurityRestService}. + */ +@Named +@Transactional +public class SecurityRestServiceImpl implements SecurityRestService { + + /** Logger instance. */ + private static final Logger LOG = LoggerFactory.getLogger(SecurityRestServiceImpl.class); + + /** + * Use {@link CsrfTokenRepository} for CSRF protection. + */ + private CsrfTokenRepository csrfTokenRepository; + + @Override + @PermitAll + public CsrfToken getCsrfToken(HttpServletRequest request, HttpServletResponse response) { + + CsrfToken token = this.csrfTokenRepository.loadToken(request); + if (token == null) { + LOG.error("No CsrfToken could be found - instantiating a new Token"); + token = this.csrfTokenRepository.generateToken(request); + this.csrfTokenRepository.saveToken(token, request, response); + } + return token; + } + + @Override + @PermitAll + public UserProfileTo getCurrentUser() { + + SecurityContext context = SecurityContextHolder.getContext(); + Authentication authentication = null; + if (context != null) { + authentication = context.getAuthentication(); + } + if (authentication == null) { + throw new NoActiveUserException(); + } + UserDetails user = (UserDetails) authentication.getPrincipal(); + UserProfileTo profile = new UserProfileTo(); + profile.setLogin(user.getUsername()); + return profile; + } + + /** + * @param csrfTokenRepository the csrfTokenRepository to set + */ + @Inject + public void setCsrfTokenRepository(CsrfTokenRepository csrfTokenRepository) { + + this.csrfTokenRepository = csrfTokenRepository; + } +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/META-INF/cxf/org.apache.cxf.Logger b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/META-INF/cxf/org.apache.cxf.Logger new file mode 100644 index 00000000..27dd788b --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/META-INF/cxf/org.apache.cxf.Logger @@ -0,0 +1 @@ +org.apache.cxf.common.logging.Slf4jLogger \ No newline at end of file diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/META-INF/orm.xml b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/META-INF/orm.xml new file mode 100644 index 00000000..d7b33e22 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/META-INF/orm.xml @@ -0,0 +1,21 @@ + + + #if ($dbType == 'mysql' || $dbType == 'mariadb') + + + + + + + + + + + + + + + + #end + \ No newline at end of file diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/application.properties b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/application.properties new file mode 100644 index 00000000..cdfd8cc4 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/application.properties @@ -0,0 +1,73 @@ +# This is the configuration file shipped with the application that contains reasonable defaults. +# Environment specific configurations are configured in config/application.properties. +# If you are running in a servlet container you may add this to lib/config/application.properties in case you do not +# want to touch the WAR file. + +# server.port=8080 + +spring.application.name=${rootArtifactId} +server.context-path=/ + +security.expose.error.details=false +security.cors.enabled=false +spring.jpa.hibernate.ddl-auto=validate + +# Datasource for accessing the database +# https://github.com/spring-projects/spring-boot/blob/d3c34ee3d1bfd3db4a98678c524e145ef9bca51c/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/DatabaseDriver.java +#if ($dbType == 'mariadb') +spring.jpa.database=mysql +#elseif ($dbType == 'hana') +# Requires spring 5.1 - see https://jira.spring.io/browse/SPR-16460 +#spring.jpa.database=hana +spring.jpa.database=default +#else +spring.jpa.database=${dbType} +#end +#if ($dbType == 'h2') +# spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +# spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=sa +#elseif ($dbType == 'hsqldb') +# spring.jpa.database-platform=org.hibernate.dialect.HSQLDialect +# spring.datasource.driver-class-name=org.hsqldb.jdbc.JDBCDriver +spring.datasource.username=SA +#elseif ($dbType == 'postgresql') +# spring.jpa.database-platform=org.hibernate.dialect.PostgreSQL9Dialect +# spring.datasource.driver-class-name=org.postgresql.Driver +spring.datasource.username=${rootArtifactId} +#elseif ($dbType == 'mysql') +# spring.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect +# spring.datasource.driver-class-name=com.mysql.jdbc.Driver +spring.datasource.username=${rootArtifactId} +#elseif ($dbType == 'mariadb') +# spring.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect +# spring.datasource.driver-class-name=org.mariadb.jdbc.Driver +spring.datasource.username=${rootArtifactId} +#elseif ($dbType == 'hana') +# spring.jpa.database-platform=org.hibernate.dialect.HANAColumnStoreDialect +# spring.datasource.driver-class-name=com.sap.db.jdbc.Driver +spring.datasource.username=${rootArtifactId} +#elseif ($dbType == 'oracle') +# spring.jpa.database-platform=org.hibernate.dialect.Oracle10gDialect +# spring.jpa.database-platform=org.hibernate.dialect.Oracle12cDialect +# spring.datasource.driver-class-name=oracle.jdbc.OracleDriver +spring.datasource.username=${rootArtifactId} +#else +spring.datasource.username=${rootArtifactId} +#end + +# Hibernate NamingStrategy has been deprecated and then removed in favor of two step naming strategy ImplicitNamingStrategy and PhysicalNamingStrategy +spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl +spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl + +# to prevent that Spring Boot launches batch jobs on startup +# might otherwise lead to errors if job parameters are needed (or lead to unwanted modifications and longer startup times) +# see http://stackoverflow.com/questions/22318907/how-to-stop-spring-batch-scheduled-jobs-from-running-at-first-time-when-executin +spring.batch.job.enabled=false + +# Flyway for Database Setup and Migrations +#if ($dbType == 'mariadb') +spring.flyway.locations=classpath:db/migration,classpath:db/type/mysql +#else +spring.flyway.locations=classpath:db/migration,classpath:db/type/${dbType} +#end diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/config/app/common/dozer-mapping.xml b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/config/app/common/dozer-mapping.xml new file mode 100644 index 00000000..032664c6 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/config/app/common/dozer-mapping.xml @@ -0,0 +1,37 @@ + + + + + + true + + + + java.lang.Long + java.lang.Integer + java.lang.Number + + + + + + + + + ${package}.general.dataaccess.api.ApplicationPersistenceEntity + com.devonfw.module.basic.common.api.to.AbstractEto + + this + persistentEntity + + + diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/config/app/dataaccess/NamedQueries.xml b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/config/app/dataaccess/NamedQueries.xml new file mode 100644 index 00000000..ee83c8c6 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/config/app/dataaccess/NamedQueries.xml @@ -0,0 +1,5 @@ + + + + diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/config/app/security/access-control-schema.xml b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/config/app/security/access-control-schema.xml new file mode 100644 index 00000000..418f6ebe --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/config/app/security/access-control-schema.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/config/application.properties b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/config/application.properties new file mode 100644 index 00000000..5dc4e65a --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/config/application.properties @@ -0,0 +1,48 @@ +# This is the spring boot configuration file for development. It will not be included into the application. +# In order to set specific configurations in a regular installed environment create an according file +# config/application.properties in the server. If you are deploying the application to a servlet container as untouched +# WAR file you can locate this config folder in ${symbol_dollar}{CATALINA_BASE}/lib. If you want to deploy multiple applications to +# the same container (not recommended by default) you need to ensure the WARs are extracted in webapps folder and locate +# the config folder inside the WEB-INF/classes folder of the webapplication. + +server.port=8081 +server.context-path=/ + +# Datasource for accessing the database +# See https://github.com/oasp/oasp4j/wiki/guide-configuration#security +#jasypt.encryptor.password=none +#spring.datasource.password=ENC(7CnHiadYc0Wh2FnWADNjJg==) +#if ($dbType == 'h2') +spring.datasource.password= +spring.datasource.url=jdbc:h2:./.${rootArtifactId}; +#elseif ($dbType == 'hsqldb') +spring.datasource.password= +spring.datasource.url=jdbc:hsqldb:file:./.${rootArtifactId} +#elseif ($dbType == 'postgresql') +spring.datasource.password=todo +spring.datasource.url=jdbc:postgresql://localhost/db +spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true +#elseif ($dbType == 'mysql') +spring.datasource.password=todo +spring.datasource.url=jdbc:mysql://address=(protocol=tcp)(host=localhost)(port=3306)/db +#elseif ($dbType == 'mariadb') +spring.datasource.password=todo +spring.datasource.url=jdbc:mariadb://localhost:3306/db +#elseif ($dbType == 'hana') +spring.datasource.password=todo +# https://help.sap.com/viewer/0eec0d68141541d1b07893a39944924e/latest/en-US/b250e7fef8614ea0a0973d58eb73bda8.html +spring.datasource.url=jdbc:sap://localhost:39015 +#elseif ($dbType == 'oracle') +spring.datasource.password=todo +spring.datasource.url=jdbc:oracle:thin:@localhost:1521/XE +#else +spring.datasource.password=todo +spring.datasource.url=jdbc:${dbType}:TODO +#end + +# Enable JSON pretty printing +spring.jackson.serialization.INDENT_OUTPUT=true + +# Flyway for Database Setup and Migrations +spring.flyway.enabled=true +spring.flyway.clean-on-validation-error=true diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/migration/1.0/V0004__Add_blob_data.sql b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/migration/1.0/V0004__Add_blob_data.sql new file mode 100644 index 00000000..574db16c --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/migration/1.0/V0004__Add_blob_data.sql @@ -0,0 +1 @@ +INSERT INTO BinaryObject(id, ModificationCounter, filesize, content, mimeType) VALUES (10, 0, 1150 ,'00000100010010100000010020006804000016000000280000001000000020000000010020000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000ECBD48EFECBD48FFECBD48FFECBD48FFECBD48FFECBD48F90000000000000000000000000000000000000000000000000000000000000000ECBD48FAECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FF0000000000000000000000000000000000000000ECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FF000000000000000000000000ECBD48FAECBD48FFFFFFFFFFECBD48FFFFFFFFFFFFFFFFFFEEC35EFFFFFEFDFFFFFFFFFFFFFFFFFFFFFFFFFFEFC86BFFECBD48FFECBD48FF0000000000000000ECBD48FFECBD48FFF4D696FFECBD48FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFECBD48FFECBD48FF00000000ECBD48EFECBD48FFECBD48FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1CB7FFFFFFFFFFFFFFFFFFFECBD48FFECBD48FFFFFFFFFFECBD48FFECBD48FFECBD48F9ECBD48FFECBD48FFECBD48FFFFFFFFFFFFFFFFFFFDF8EFFFECBD48FFFFFFFFFFFFFFFFFFFFFFFFFFECBD48FFECBD48FFFFFFFFFFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFFFFFFFFFFFFFFFFFFFFFFFFFECBD48FFECBD48FFFFFFFFFFFFFFFFFFFFFFFFFFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFFFFFFFFFFFFFFFFFFFFFFFFFECBD48FFECBD48FFECBD48FFECBD48FFECBD4AFFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFFFFFFFFFFEF9F3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFECBD48FFECBD48FFECBD48FFECBD48F9ECBD48FFECBD48FFFFFFFFFFEEC45FFFECBD48FFF1CD7EFFFFFFFFFFFFFFFFFFFFFFFFFFEDC156FFFFFFFFFFFFFFFFFFECBD48FFECBD48FFECBD48EF00000000ECBD48FFECBD48FFFFFFFFFFFEF9F2FFECBD48FFFCF5E8FFFFFFFFFFFFFFFFFFFFFFFFFFECBD48FFFFFFFFFFFFFFFFFFECBD48FFECBD48FF0000000000000000ECBD48FFECBD48FFEFC769FFFFFFFFFFFFFFFFFFFFFFFFFFEEC35EFFEEC25CFFFFFFFFFFFFFFFFFFFFFFFFFFEFC86BFFECBD48FFECBD48FA000000000000000000000000ECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FF0000000000000000000000000000000000000000ECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FFECBD48FA0000000000000000000000000000000000000000000000000000000000000000ECBD48F9ECBD48FFECBD48FFECBD48FFECBD48FFECBD48EF0000000000000000000000000000000000000000F81F0000E0070000C003000080010000800100000000000000000000000000000000000000000000000000008001000080010000C0030000E0070000F81F0000', 'image/vnd.microsoft.icon'); diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/h2/V0001__Create_Sequence.sql b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/h2/V0001__Create_Sequence.sql new file mode 100644 index 00000000..22104389 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/h2/V0001__Create_Sequence.sql @@ -0,0 +1,2 @@ +-- Leave a large ID space reserved for master-data and test-data +CREATE SEQUENCE HIBERNATE_SEQUENCE START WITH 1000000; diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/h2/V0002__Create_RevInfo.sql b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/h2/V0002__Create_RevInfo.sql new file mode 100644 index 00000000..8a41c6f9 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/h2/V0002__Create_RevInfo.sql @@ -0,0 +1,6 @@ +-- *** RevInfo (Commit log for envers audit trail) *** +CREATE TABLE RevInfo( + id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1), + "timestamp" BIGINT NOT NULL, + userLogin VARCHAR(255) +); \ No newline at end of file diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/h2/V0003__Create_BinaryObject.sql b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/h2/V0003__Create_BinaryObject.sql new file mode 100644 index 00000000..d91838ce --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/h2/V0003__Create_BinaryObject.sql @@ -0,0 +1,9 @@ +-- *** BinaryObject (BLOBs) *** +CREATE TABLE BinaryObject ( + id BIGINT NOT NULL AUTO_INCREMENT, + modificationCounter INTEGER NOT NULL, + content BLOB(2147483647), + filesize BIGINT NOT NULL, + mimeType VARCHAR(255), + PRIMARY KEY (ID) +); diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/hana/V0001__Create_Sequence.sql b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/hana/V0001__Create_Sequence.sql new file mode 100644 index 00000000..49953d2c --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/hana/V0001__Create_Sequence.sql @@ -0,0 +1,18 @@ +-- Leave a large ID space reserved for master-data and test-data +CREATE SEQUENCE HIBERNATE_SEQUENCE START WITH 1000000; + +-- hana does not support Dateadd function out of the box so we add it here to be able to use it for master-data SQLs +CREATE FUNCTION DATEADD(IN DATETYPE NVARCHAR(256), IN NUMBER INTEGER, IN TS TIMESTAMP) +RETURNS TSADD TIMESTAMP +AS +BEGIN + IF :DATETYPE = 'DAY' + THEN + TSADD = ADD_DAYS(:TS, :NUMBER); + ELSEIF :DATETYPE = 'HOUR' + THEN + TSADD = ADD_SECONDS(:TS, :NUMBER * 3600); + ELSE + SIGNAL SQL_ERROR_CODE 10000 SET MESSAGE_TEXT = 'Unsupported date type: ' || :DATETYPE; + END IF; +END; \ No newline at end of file diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/hana/V0002__Create_RevInfo.sql b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/hana/V0002__Create_RevInfo.sql new file mode 100644 index 00000000..38b650b3 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/hana/V0002__Create_RevInfo.sql @@ -0,0 +1,7 @@ +-- *** RevInfo (Commit log for envers audit trail) *** +CREATE COLUMN TABLE RevInfo( + id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1), + "timestamp" BIGINT NOT NULL, + userLogin VARCHAR(255), + PRIMARY KEY (ID) +); diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/hana/V0003__Create_BinaryObject.sql b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/hana/V0003__Create_BinaryObject.sql new file mode 100644 index 00000000..6463189d --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/hana/V0003__Create_BinaryObject.sql @@ -0,0 +1,9 @@ +-- *** BinaryObject (BLOBs) *** +CREATE COLUMN TABLE BinaryObject ( + id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, + modificationCounter INTEGER NOT NULL, + content BLOB, + filesize BIGINT NOT NULL, + mimeType VARCHAR(255), + CONSTRAINT PK_BinaryObject_id PRIMARY KEY(ID) +); diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/mssql/V0001__Create_Sequence.sql b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/mssql/V0001__Create_Sequence.sql new file mode 100644 index 00000000..80704fad --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/mssql/V0001__Create_Sequence.sql @@ -0,0 +1 @@ +-- no sequences are used in MS-SQL Server instead use IDENTITY(«seed»,1) for every ID diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/mssql/V0002__Create_RevInfo.sql b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/mssql/V0002__Create_RevInfo.sql new file mode 100644 index 00000000..ceae13d0 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/mssql/V0002__Create_RevInfo.sql @@ -0,0 +1,6 @@ +-- *** RevInfo (Commit log for envers audit trail) *** +CREATE TABLE REVINFO( + id BIGINT NOT NULL IDENTITY(1,1), + timestamp BIGINT NOT NULL, + userLogin VARCHAR(255) +); diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/mssql/V0003__Create_BinaryObject.sql b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/mssql/V0003__Create_BinaryObject.sql new file mode 100644 index 00000000..4a454bee --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/mssql/V0003__Create_BinaryObject.sql @@ -0,0 +1,9 @@ +-- *** BinaryObject (BLOBs) *** +CREATE TABLE BINARYOBJECT ( + id BIGINT NOT NULL IDENTITY(10,1), + modificationCounter INTEGER NOT NULL, + content varbinary(max), + filesize BIGINT NOT NULL, + mimeType VARCHAR(255), + PRIMARY KEY (ID) +); diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/mysql/V0001__Create_Sequence.sql b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/mysql/V0001__Create_Sequence.sql new file mode 100644 index 00000000..c5da8cf7 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/mysql/V0001__Create_Sequence.sql @@ -0,0 +1 @@ +-- no sequences are used in MySQL/MariaDB instead use AUTO_INCREMENT for every ID diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/mysql/V0002__Create_RevInfo.sql b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/mysql/V0002__Create_RevInfo.sql new file mode 100644 index 00000000..a9590a2b --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/mysql/V0002__Create_RevInfo.sql @@ -0,0 +1,6 @@ +-- *** RevInfo (Commit log for envers audit trail) *** +CREATE TABLE REVINFO( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + timestamp BIGINT NOT NULL, + userLogin VARCHAR(255) +); diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/mysql/V0003__Create_BinaryObject.sql b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/mysql/V0003__Create_BinaryObject.sql new file mode 100644 index 00000000..ff7ca5f7 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/mysql/V0003__Create_BinaryObject.sql @@ -0,0 +1,8 @@ +-- *** BinaryObject (BLOBs) *** +CREATE TABLE BINARYOBJECT ( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + modificationCounter INT NOT NULL, + content LONGBLOB, + filesize BIGINT NOT NULL, + mimeType VARCHAR(255) +); diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/oracle/V0001__Create_Sequence.sql b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/oracle/V0001__Create_Sequence.sql new file mode 100644 index 00000000..22104389 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/oracle/V0001__Create_Sequence.sql @@ -0,0 +1,2 @@ +-- Leave a large ID space reserved for master-data and test-data +CREATE SEQUENCE HIBERNATE_SEQUENCE START WITH 1000000; diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/oracle/V0002__Create_RevInfo.sql b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/oracle/V0002__Create_RevInfo.sql new file mode 100644 index 00000000..a459bf24 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/oracle/V0002__Create_RevInfo.sql @@ -0,0 +1,7 @@ +-- *** RevInfo (Commit log for envers audit trail) *** +CREATE TABLE RevInfo ( + id NUMBER(19), + "timestamp" NUMBER(19,0), + userLogin VARCHAR2(255 CHAR), + CONSTRAINT PK_RevInfo_id PRIMARY KEY (id) +); \ No newline at end of file diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/oracle/V0003__Create_BinaryObject.sql b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/oracle/V0003__Create_BinaryObject.sql new file mode 100644 index 00000000..e5635f1b --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/oracle/V0003__Create_BinaryObject.sql @@ -0,0 +1,9 @@ +-- *** BinaryObject (BLOBs) *** +CREATE TABLE BinaryObject ( + id NUMBER(19), + modificationCounter NUMBER(10, 0) NOT NULL, + content BLOB, + filesize NUMBER(10, 0) NOT NULL, + mimeType VARCHAR(255), + CONSTRAINT PK_BinaryObject_id PRIMARY KEY (ID) +); diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/postgresql/V0001__Create_Sequence.sql b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/postgresql/V0001__Create_Sequence.sql new file mode 100644 index 00000000..22104389 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/postgresql/V0001__Create_Sequence.sql @@ -0,0 +1,2 @@ +-- Leave a large ID space reserved for master-data and test-data +CREATE SEQUENCE HIBERNATE_SEQUENCE START WITH 1000000; diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/postgresql/V0002__Create_RevInfo.sql b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/postgresql/V0002__Create_RevInfo.sql new file mode 100644 index 00000000..b6c8663c --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/postgresql/V0002__Create_RevInfo.sql @@ -0,0 +1,6 @@ +-- *** RevInfo (Commit log for envers audit trail) *** +CREATE TABLE REVINFO( + id BIGINT NOT NULL, + timestamp BIGINT NOT NULL, + userLogin VARCHAR(255) +); diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/postgresql/V0003__Create_BinaryObject.sql b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/postgresql/V0003__Create_BinaryObject.sql new file mode 100644 index 00000000..54bb1614 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/db/type/postgresql/V0003__Create_BinaryObject.sql @@ -0,0 +1,9 @@ +-- *** BinaryObject (BLOBs) *** +CREATE TABLE BINARYOBJECT ( + id BIGSERIAL NOT NULL, + modificationCounter INTEGER NOT NULL, + content BYTEA, + filesize BIGINT NOT NULL, + mimeType VARCHAR(255), + PRIMARY KEY (ID) +); diff --git a/templates/server/src/main/resources/archetype-resources/core/src/main/resources/static/index.html b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/static/index.html new file mode 100644 index 00000000..88b7063e --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/main/resources/static/index.html @@ -0,0 +1,18 @@ + + + +Welcome + + +

Welcome

+ This is a test! +
+ Services Overview (CXF) +
+
+ +
+ +
+ + \ No newline at end of file diff --git a/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/common/base/Devon4jPackageCheckTest.java b/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/common/base/Devon4jPackageCheckTest.java new file mode 100644 index 00000000..1b249a0c --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/common/base/Devon4jPackageCheckTest.java @@ -0,0 +1,66 @@ +package ${package}.general.common.base; + +import java.util.HashSet; +import java.util.Set; + +import net.sf.mmm.util.reflect.api.ReflectionUtil; +import net.sf.mmm.util.reflect.base.ReflectionUtilImpl; + +import org.assertj.core.api.SoftAssertions; +import org.junit.Test; + +import com.devonfw.module.basic.common.api.reflect.Devon4jPackage; +import com.devonfw.module.test.common.base.ModuleTest; + +/** + * This test verifies that the entire code of your code-base is located in {@link Devon4jPackage#isValid() valid Devon4j + * packages}. + */ +public class Devon4jPackageCheckTest extends ModuleTest { + + /** + * Scans all the packages of this application root pacakge namespace. Will verify that these are + * {@link Devon4jPackage#isValid() valid Devon4j packages}. + */ + @Test + public void testPackages() { + + Devon4jPackage pkg = Devon4jPackage.of(Devon4jPackageCheckTest.class); + assertThat(pkg.isValid()).isTrue(); + + ReflectionUtil ru = ReflectionUtilImpl.getInstance(); + Set classNames = ru.findClassNames(getRootPackage2Scan(pkg), true); + String appPackage = pkg.getRoot() + "." + pkg.getApplication(); + Set packages = new HashSet<>(128); + packages.add(appPackage); // allow SpringBootApp, etc. in application package + SoftAssertions assertion = new SoftAssertions(); + for (String className : classNames) { + int lastDot = className.lastIndexOf('.'); + if (lastDot > 0) { + String packageName = className.substring(0, lastDot); + boolean added = packages.add(packageName); + if (added) { + pkg = Devon4jPackage.of(packageName); + if (!pkg.isValid()) { + assertion.assertThat(pkg.isValid()) + .as("package " + packageName + " is invalid (component: " + pkg.getComponent() + ", layer: " + + pkg.getLayer() + ", scope: " + pkg.getScope() + "). Hint contains e.g. " + + className.substring(lastDot + 1)) + .isTrue(); + } + } + } + } + assertion.assertAll(); + } + + /** + * @param pkg the {@link Devon4jPackage} of this test. + * @return the root package to scan for {@link Class}es to get the actual packages to check. + */ + protected String getRootPackage2Scan(Devon4jPackage pkg) { + + return pkg.getRoot() + "." + pkg.getApplication(); + } + +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/common/base/PermissionCheckTest.java b/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/common/base/PermissionCheckTest.java new file mode 100644 index 00000000..e9a77b4d --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/common/base/PermissionCheckTest.java @@ -0,0 +1,67 @@ +package ${package}.general.common.base; + +import com.devonfw.module.test.common.base.ModuleTest; + +import java.lang.reflect.Method; +import java.util.Set; + +import javax.annotation.security.DenyAll; +import javax.annotation.security.PermitAll; +import javax.annotation.security.RolesAllowed; + +import net.sf.mmm.util.filter.api.Filter; +import net.sf.mmm.util.reflect.api.ReflectionUtil; +import net.sf.mmm.util.reflect.base.ReflectionUtilImpl; + +import org.assertj.core.api.SoftAssertions; +import org.junit.Test; + +/** + * Tests the permission check in logic layer. + */ +public class PermissionCheckTest extends ModuleTest { + + /** + * Check if all relevant methods in use case implementations have permission checks i.e. {@link RolesAllowed}, + * {@link DenyAll} or {@link PermitAll} annotation is applied. This is only checked for methods that are declared in + * the corresponding interface and thus have the {@link Override} annotations applied. + */ + @Test + public void permissionCheckAnnotationPresent() { + + String packageName = "${package}"; + Filter filter = new Filter() { + + @Override + public boolean accept(String value) { + + return value.contains(".logic.impl.usecase.Uc") && value.endsWith("Impl"); + } + + }; + ReflectionUtil ru = ReflectionUtilImpl.getInstance(); + Set classNames = ru.findClassNames(packageName, true, filter); + Set> classes = ru.loadClasses(classNames); + SoftAssertions assertions = new SoftAssertions(); + for (Class clazz : classes) { + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + Method parentMethod = ru.getParentMethod(method); + if (parentMethod != null) { + Class declaringClass = parentMethod.getDeclaringClass(); + if (declaringClass.isInterface() && declaringClass.getSimpleName().startsWith("Uc")) { + boolean hasAnnotation = false; + if (method.getAnnotation(RolesAllowed.class) != null || method.getAnnotation(DenyAll.class) != null + || method.getAnnotation(PermitAll.class) != null) { + hasAnnotation = true; + } + assertions.assertThat(hasAnnotation) + .as("Method " + method.getName() + " in Class " + clazz.getSimpleName() + " is missing access control") + .isTrue(); + } + } + } + } + assertions.assertAll(); + } +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/common/base/test/AccessControlSchemaXmlValidationTest.java b/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/common/base/test/AccessControlSchemaXmlValidationTest.java new file mode 100644 index 00000000..e11595d8 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/common/base/test/AccessControlSchemaXmlValidationTest.java @@ -0,0 +1,57 @@ +package ${package}.general.common.base.test; + +import com.devonfw.module.test.common.base.ModuleTest; + +import java.io.File; +import java.io.IOException; +import java.net.URL; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.dom.DOMSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; + +import org.junit.Test; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +/** + * Class for XML Validation Tests. + * + */ +public class AccessControlSchemaXmlValidationTest extends ModuleTest { + + /** + * Tests if the access-control-schema.xml is valid. + * + * @throws ParserConfigurationException If a DocumentBuilder cannot be created which satisfies the configuration + * requested. + * @throws IOException If any IO errors occur. + * @throws SAXException If an error occurs during the validation. + */ + @Test + public void validateAccessControllSchema() throws ParserConfigurationException, SAXException, IOException { + + // parse an XML document into a DOM tree + DocumentBuilder parser = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + String xmlPath = getClass().getResource("/config/app/security/access-control-schema.xml").getPath(); + Document document = parser.parse(new File(xmlPath)); + + // create a SchemaFactory capable of understanding WXS schemas + SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + + // load a WXS schema, represented by a Schema instance + URL schemaPath = getClass().getResource("/com/devonfw/module/security/access-control-schema.xsd"); + Schema schema = factory.newSchema(schemaPath); + + // create a Validator instance, which can be used to validate an instance document + Validator validator = schema.newValidator(); + + // validate the DOM tree + validator.validate(new DOMSource(document)); + } +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/common/base/test/ApplicationComponentTest.java b/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/common/base/test/ApplicationComponentTest.java new file mode 100644 index 00000000..acb0ede1 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/common/base/test/ApplicationComponentTest.java @@ -0,0 +1,22 @@ +package ${package}.general.common.base.test; + +import com.devonfw.module.test.common.base.ComponentTest; + +import ${package}.SpringBootApp; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; + +/** + * Abstract base class for {@link ComponentTest}s of this application. + */ +@SpringBootTest(classes = { SpringBootApp.class }, webEnvironment = WebEnvironment.NONE) +public abstract class ApplicationComponentTest extends ComponentTest { + + @Override + protected void doTearDown() { + super.doTearDown(); + TestUtil.logout(); + } + +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/common/base/test/ApplicationSubsystemTest.java b/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/common/base/test/ApplicationSubsystemTest.java new file mode 100644 index 00000000..f7442443 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/common/base/test/ApplicationSubsystemTest.java @@ -0,0 +1,16 @@ +package ${package}.general.common.base.test; + +import com.devonfw.module.test.common.base.SubsystemTest; + +import ${package}.SpringBootApp; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; + +/** + * Abstract base class for {@link SubsystemTest}s of this application. + */ +@SpringBootTest(classes = { SpringBootApp.class }, webEnvironment = WebEnvironment.RANDOM_PORT) +public abstract class ApplicationSubsystemTest extends SubsystemTest { + +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/common/base/test/DbTestHelper.java b/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/common/base/test/DbTestHelper.java new file mode 100644 index 00000000..c98b2cb4 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/common/base/test/DbTestHelper.java @@ -0,0 +1,60 @@ +package ${package}.general.common.base.test; + +import javax.inject.Named; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.MigrationVersion; + +/** + * This class provides methods for handling the database during testing where resets (and other operations) may be + * necessary. + */ +@Named +public class DbTestHelper { + + private Flyway flyway; + + private MigrationVersion migrationVersion; + + /** + * The constructor. + * + * @param flyway an instance of type {@link Flyway}. + */ + public DbTestHelper(Flyway flyway) { + super(); + this.flyway = flyway; + } + + /** + * Drops the whole database. + */ + public void dropDatabase() { + + this.flyway.clean(); + } + + /** + * Calls {@link #dropDatabase()} internally, and migrates to the highest available migration (default) or to the + * {@code migrationVersion} specified by {@link #setMigrationVersion(String)}. + */ + public void resetDatabase() { + + dropDatabase(); + if (this.migrationVersion != null) { + this.flyway.setTarget(this.migrationVersion); + } + this.flyway.migrate(); + } + + /** + * This method sets the internal value of the {@code migrationVersion}. + * + * @param migrationVersion new {@code String} value of {@code migrationVersion}. Must not be null + */ + public void setMigrationVersion(String migrationVersion) { + + this.migrationVersion = MigrationVersion.fromVersion(migrationVersion); + } + +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/common/base/test/TestUtil.java b/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/common/base/test/TestUtil.java new file mode 100644 index 00000000..a3fd2687 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/common/base/test/TestUtil.java @@ -0,0 +1,29 @@ +package ${package}.general.common.base.test; + +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * This is a utility for testing. It allows to simulate authentication for component testing. + */ +public class TestUtil { + + /** + * @param login the id of the user to run the test as. + * @param permissions the permissions for the test. + */ + public static void login(String login, String... permissions) { + + Authentication authentication = new TestingAuthenticationToken(login, login, permissions); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + /** + * Logs off any previously logged on user. + */ + public static void logout() { + + SecurityContextHolder.getContext().setAuthentication(null); + } +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/common/impl/config/TestWebSecurityConfig.java b/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/common/impl/config/TestWebSecurityConfig.java new file mode 100644 index 00000000..927c50e0 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/common/impl/config/TestWebSecurityConfig.java @@ -0,0 +1,54 @@ +package ${package}.general.common.impl.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; + +import ${package}.general.service.impl.config.BaseWebSecurityConfig; +import com.devonfw.module.basic.common.api.config.SpringProfileConstants; + +/** + * This type provides web security configuration for testing purposes. + */ +@Configuration +@EnableWebSecurity +@Profile(SpringProfileConstants.JUNIT) +public class TestWebSecurityConfig extends BaseWebSecurityConfig { + private static Logger LOG = LoggerFactory.getLogger(TestWebSecurityConfig.class); + + /** + * Configure spring security to enable a simple webform-login + a simple rest login. + */ + @Override + public void configure(HttpSecurity http) throws Exception { + + super.configure(http); + http.addFilterBefore(basicAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); + + // Disable CSRF protection in tests for simpler testing of REST services + http.csrf().disable(); + LOG.debug("*** CSRF disabled - this config should only be used in development environment ***"); + } + + /** + * @return {@link BasicAuthenticationFilter}. + * @throws Exception on initialization error. + */ + @Bean + protected BasicAuthenticationFilter basicAuthenticationFilter() throws Exception { + + AuthenticationEntryPoint authenticationEntryPoint = new BasicAuthenticationEntryPoint(); + BasicAuthenticationFilter basicAuthenticationFilter = + new BasicAuthenticationFilter(authenticationManagerBean(), authenticationEntryPoint); + return basicAuthenticationFilter; + } + +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/service/base/test/RestServiceTest.java b/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/service/base/test/RestServiceTest.java new file mode 100644 index 00000000..b255cb1d --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/service/base/test/RestServiceTest.java @@ -0,0 +1,66 @@ +package ${package}.general.service.base.test; + +import javax.inject.Inject; + +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; + +import ${package}.SpringBootApp; +import ${package}.general.common.base.test.DbTestHelper; +import ${package}.general.common.base.test.TestUtil; + +import com.devonfw.module.test.common.base.SubsystemTest; +import com.devonfw.module.service.common.api.client.ServiceClientFactory; + +/** + * Abstract base class for {@link SubsystemTest}s which runs the tests within a local server.
+ *
+ * The local server's port is randomly assigned. + */ +@SpringBootTest(classes = { SpringBootApp.class }, webEnvironment = WebEnvironment.RANDOM_PORT) +public abstract class RestServiceTest extends SubsystemTest { + + /** + * The port of the web server during the test. + */ + @LocalServerPort + protected int port; + + @Inject + private ServiceClientFactory serviceClientFactory; + + @Inject + private DbTestHelper dbTestHelper; + + @Override + protected void doSetUp() { + + super.doSetUp(); + } + + @Override + protected void doTearDown() { + + super.doTearDown(); + TestUtil.logout(); + } + + /** + * @return the {@link DbTestHelper} + */ + protected DbTestHelper getDbTestHelper() { + + return this.dbTestHelper; + } + + + /** + * @return the {@link ServiceClientFactory} + */ + protected ServiceClientFactory getServiceClientFactory() { + + return this.serviceClientFactory; + } + +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/service/impl/rest/SecurityRestServiceImplTest.java b/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/service/impl/rest/SecurityRestServiceImplTest.java new file mode 100644 index 00000000..d4bbff8e --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/test/java/__packageInPathFormat__/general/service/impl/rest/SecurityRestServiceImplTest.java @@ -0,0 +1,96 @@ +package ${package}.general.service.impl.rest; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.client.RestTemplate; + +import com.devonfw.module.service.common.api.client.config.ServiceClientConfigBuilder; + +import ${package}.general.common.api.to.UserProfileTo; +import ${package}.general.service.api.rest.SecurityRestService; +import ${package}.general.service.base.test.RestServiceTest; + +/** + * This class tests the login functionality of {@link SecurityRestServiceImpl}. + */ +@RunWith(SpringRunner.class) +public class SecurityRestServiceImplTest extends RestServiceTest { + + /** Logger instance. */ + private static final Logger LOG = LoggerFactory.getLogger(SecurityRestServiceImplTest.class); + + /** + * Test the login functionality as it will be used from a JavaScript client. + */ + @Test + public void testLogin() { + + String login = "waiter"; + String password = "waiter"; + + ResponseEntity postResponse = login(login, password); + LOG.debug("Body: " + postResponse.getBody()); + assertThat(postResponse.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(postResponse.getHeaders().containsKey(HttpHeaders.SET_COOKIE)).isTrue(); + } + + /** + * Test of {@code SecurityRestService.getCsrfToken()}. + */ + @Test + public void testGetCsrfToken() { + + String login = "waiter"; + String password = "waiter"; + SecurityRestService securityService = getServiceClientFactory().create(SecurityRestService.class, + new ServiceClientConfigBuilder().host("localhost").authBasic().userLogin(login).userPassword(password) + .buildMap()); + CsrfToken csrfToken = securityService.getCsrfToken(null, null); + assertThat(csrfToken.getHeaderName()).isEqualTo("X-CSRF-TOKEN"); + assertThat(csrfToken.getParameterName()).isEqualTo("_csrf"); + assertThat(csrfToken.getToken()).isNotNull(); + LOG.debug("Csrf Token: {}", csrfToken.getToken()); + } + + /** + * Test of {@link SecurityRestService#getCurrentUser()}. + */ + @Test + public void testGetCurrentUser() { + String login = "waiter"; + String password = "waiter"; + SecurityRestService securityService = getServiceClientFactory().create(SecurityRestService.class, + new ServiceClientConfigBuilder().host("localhost").authBasic().userLogin(login).userPassword(password) + .buildMap()); + UserProfileTo userProfile = securityService.getCurrentUser(); + assertThat(userProfile.getLogin()).isEqualTo(login); + } + + /** + * Performs the login as required by a JavaScript client. + * + * @param userName the username of the user + * @param tmpPassword the password of the user + * @return @ {@link ResponseEntity} containing containing a cookie in its header. + */ + private ResponseEntity login(String userName, String tmpPassword) { + + String tmpUrl = "http://localhost:" + String.valueOf(this.port) + "/services/rest/login"; + + HttpEntity postRequest = new HttpEntity<>( + "{\"j_username\": \"" + userName + "\", \"j_password\": \"" + tmpPassword + "\"}", new HttpHeaders()); + + ResponseEntity postResponse = new RestTemplate().exchange(tmpUrl, HttpMethod.POST, postRequest, String.class); + return postResponse; + } + +} diff --git a/templates/server/src/main/resources/archetype-resources/core/src/test/resources/config/application.properties b/templates/server/src/main/resources/archetype-resources/core/src/test/resources/config/application.properties new file mode 100644 index 00000000..47b72ef4 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/core/src/test/resources/config/application.properties @@ -0,0 +1,13 @@ +# This is the spring boot configuration file for JUnit tests. It will only affect JUnits and is not included into the application. +spring.profiles.active=junit + +# Database and JPA +spring.jpa.database=h2 +spring.datasource.url=jdbc:h2:mem:app; +spring.datasource.password= +spring.datasource.username=sa +spring.jpa.hibernate.ddl-auto=none + +# Flyway for Database Setup and Migrations +spring.flyway.enabled=true +spring.flyway.locations=classpath:db/migration,classpath:db/type/h2 diff --git a/templates/server/src/main/resources/archetype-resources/pom.xml b/templates/server/src/main/resources/archetype-resources/pom.xml new file mode 100644 index 00000000..db7b041d --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/pom.xml @@ -0,0 +1,508 @@ + + + 4.0.0 + ${artifactId} + ${groupId} + ${version} + pom + ${project.artifactId} + Application based on the Open Application Standard Platform for Java (devon4j). + + + 2.0.4.RELEASE + $[devon4j.version] + 1.8 + UTF-8 + UTF-8 + com.devonfw.module.test.common.api.category.CategorySystemTest + + + + api + core + #if ($earProjectName != '.') + ${earProjectName} + #end + #if ($batch == 'batch') + batch + #end + server + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + + com.devonfw.java.boms + devon4j-bom + ${devon4j.version} + + pom + import + + + + + + + junit + junit + test + + + org.slf4j + slf4j-api + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${project.build.sourceEncoding} + ${java.version} + ${java.version} + + + + + maven-surefire-plugin + + ${devonfw.test.excluded.groups} + + + + + + org.jacoco + jacoco-maven-plugin + + + default-prepare-agent + + prepare-agent + + + + default-report + + report + + + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.0.2 + + + org.apache.maven.plugins + maven-compiler-plugin + 3.7.0 + + + org.apache.maven.plugins + maven-install-plugin + 2.5.2 + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + + org.apache.maven.plugins + maven-clean-plugin + 3.0.0 + + + org.apache.maven.plugins + maven-jar-plugin + 3.0.2 + + + org.apache.maven.plugins + maven-source-plugin + 3.0.1 + + + org.apache.maven.plugins + maven-site-plugin + 3.7 + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.0.0 + + + org.apache.maven.plugins + maven-antrun-plugin + 1.8 + + + org.apache.maven.plugins + maven-project-info-reports-plugin + 2.9 + + + org.apache.maven.plugins + maven-jxr-plugin + 2.5 + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.0.0 + + + + javax.interceptor + javax.interceptor-api + 1.2 + + + + + private + ${project.reporting.outputEncoding} + ${project.build.sourceEncoding} + true + + http://docs.oracle.com/javase/8/docs/api/ + + JavaDocs for ${project.name} + JavaDocs for ${project.name} + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.20.1 + + + ${basedir} + + + + org.apache.maven.plugins + maven-surefire-report-plugin + 2.20.1 + + + org.apache.maven.plugins + maven-pmd-plugin + 3.9.0 + + ${java.version} + + + + org.apache.maven.plugins + maven-war-plugin + 3.2.0 + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + org.apache.maven.plugins + maven-help-plugin + 2.2 + + + org.codehaus.mojo + cobertura-maven-plugin + 2.7 + + + org.codehaus.mojo + sonar-maven-plugin + 3.4.0.905 + + + org.codehaus.mojo + findbugs-maven-plugin + 3.0.5 + + + org.codehaus.mojo + taglist-maven-plugin + 2.4 + + + org.codehaus.mojo + flatten-maven-plugin + 1.0.1 + + + org.codehaus.mojo + servicedocgen-maven-plugin + 1.0.0-beta-3 + + + org.jacoco + jacoco-maven-plugin + 0.8.0 + + + org.owasp + dependency-check-maven + 3.1.1 + + + org.codehaus.mojo + license-maven-plugin + 1.14 + + ${project.build.directory}/generated-resources + true + true + true + true + + Apache Software License, Version 2.0|The Apache Software License, Version 2.0|Apache + 2.0|Apache License, Version 2.0 + + + + + org.springframework.boot + spring-boot-maven-plugin + 2.0.4.RELEASE + + + + + + + + moduletest + + com.devonfw.module.test.common.api.category.CategoryComponentTest,com.devonfw.module.test.common.api.category.CategorySubsystemTest,com.devonfw.module.test.common.api.category.CategorySystemTest + + + + componenttest + + com.devonfw.module.test.common.api.category.CategorySubsystemTest,com.devonfw.module.test.common.api.category.CategorySystemTest + + + + subsystemtest + + com.devonfw.module.test.common.api.category.CategorySystemTest + + + + systemtest + + + + + + doclint-disabled + + [1.8,) + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + -Xdoclint:none + + + + + + + + + security + + + + org.owasp + dependency-check-maven + + 8 + + + + + check + + + + + + + + + licenses + + + + org.codehaus.mojo + license-maven-plugin + + + aggregate-add-third-party + generate-resources + + aggregate-add-third-party + + + + + aggregate-download-licenses + generate-resources + + aggregate-download-licenses + + + + + + + + + + eclipse + + + eclipse.application + + + + eclipse-target + + + + + #if ($dbType == 'oracle') + + + maven.oracle.com + oracle-maven-repo + https://maven.oracle.com + default + + true + always + + + + + + maven.oracle.com + oracle-maven-repo + https://maven.oracle.com + default + + true + always + + + + + #end + + + + org.apache.maven.plugins + maven-project-info-reports-plugin + + false + + + + org.apache.maven.plugins + maven-jxr-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.codehaus.mojo + taglist-maven-plugin + + + TODO + @todo + FIXME + @deprecated + REVIEW + + + + + org.owasp + dependency-check-maven + + + + aggregate + check + + + + + + org.codehaus.mojo + servicedocgen-maven-plugin + + + + ${servicedoc.info.title} + ${servicedoc.info.description} + + ${servicedoc.host} + ${servicedoc.port} + ${servicedoc.basePath} + + http + + + + + + org.codehaus.mojo + license-maven-plugin + + + + third-party-report + aggregate-third-party-report + + + + + + + + diff --git a/templates/server/src/main/resources/archetype-resources/server/pom.xml b/templates/server/src/main/resources/archetype-resources/server/pom.xml new file mode 100644 index 00000000..18ed368a --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/server/pom.xml @@ -0,0 +1,150 @@ + + + 4.0.0 + + ${groupId} + ${rootArtifactId} + ${version} + + ${rootArtifactId}-server + war + ${project.artifactId} + Server for the ${rootArtifactId} application - a simple example using the Open Application Standard Platform for Java (devon4j). + + + 1.8 + + + + + ${project.groupId} + ${rootArtifactId}-core + ${project.version} + + #if ($batch == 'batch') + + ${project.groupId} + ${rootArtifactId}-batch + ${project.version} + + #end + + + + + jsclient + + + false + + + + + org.codehaus.mojo + exec-maven-plugin + + + npm-install + generate-sources + + exec + + + npm + + install + + ${js.client.dir} + + + + gulp-clean + generate-sources + + exec + + + gulp + + clean + + ${js.client.dir} + + + + gulp-build + generate-sources + + exec + + + gulp + + build:dist + + ${js.client.dir} + + + + gulp-test + test + + exec + + + gulp + + test + + ${js.client.dir} + + + + + + org.apache.maven.plugins + maven-war-plugin + + WEB-INF/classes/config/application.properties + ${project.artifactId} + false + + + + + + + + + + + ${project.basedir}/src/main/resources + + + ${js.client.dir}/dist + static + + + + + org.springframework.boot + spring-boot-maven-plugin + + ${package}.SpringBootApp + bootified + ${project.artifactId} + + + + + repackage + + + + + + + + + diff --git a/templates/server/src/main/resources/archetype-resources/server/src/main/resources/logback.xml b/templates/server/src/main/resources/archetype-resources/server/src/main/resources/logback.xml new file mode 100644 index 00000000..03813656 --- /dev/null +++ b/templates/server/src/main/resources/archetype-resources/server/src/main/resources/logback.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/server/src/test/resources/projects/basic/archetype.properties b/templates/server/src/test/resources/projects/basic/archetype.properties new file mode 100644 index 00000000..013232af --- /dev/null +++ b/templates/server/src/test/resources/projects/basic/archetype.properties @@ -0,0 +1,8 @@ +#Tue Nov 04 13:27:57 CET 2014 +package=it.pkg +version=1.0.0-SNAPSHOT +groupId=archetype.it +artifactId=basic +earProjectName=. +batch=. +dbType=h2 \ No newline at end of file diff --git a/templates/server/src/test/resources/projects/basic/goal.txt b/templates/server/src/test/resources/projects/basic/goal.txt new file mode 100644 index 00000000..f7ffc47a --- /dev/null +++ b/templates/server/src/test/resources/projects/basic/goal.txt @@ -0,0 +1 @@ +install \ No newline at end of file diff --git a/templates/server/src/test/resources/projects/batch/archetype.properties b/templates/server/src/test/resources/projects/batch/archetype.properties new file mode 100644 index 00000000..552b794c --- /dev/null +++ b/templates/server/src/test/resources/projects/batch/archetype.properties @@ -0,0 +1,8 @@ +#Tue Nov 04 13:27:57 CET 2014 +package=it.pkg +version=1.0.0-SNAPSHOT +groupId=archetype.it +artifactId=app-with-batch +earProjectName=. +batch=batch +dbType=mysql \ No newline at end of file diff --git a/templates/server/src/test/resources/projects/batch/goal.txt b/templates/server/src/test/resources/projects/batch/goal.txt new file mode 100644 index 00000000..8fdaa2de --- /dev/null +++ b/templates/server/src/test/resources/projects/batch/goal.txt @@ -0,0 +1 @@ +install -Pcomponenttest \ No newline at end of file diff --git a/templates/server/src/test/resources/projects/enterprise/archetype.properties b/templates/server/src/test/resources/projects/enterprise/archetype.properties new file mode 100644 index 00000000..a8e0f5ed --- /dev/null +++ b/templates/server/src/test/resources/projects/enterprise/archetype.properties @@ -0,0 +1,8 @@ +#Tue Nov 04 13:27:57 CET 2014 +package=it.pkg +version=1.0.0-SNAPSHOT +groupId=archetype.it +artifactId=enterprise +earProjectName=${artifactId}-ear +batch=. +dbType=postgresql \ No newline at end of file diff --git a/templates/server/src/test/resources/projects/enterprise/goal.txt b/templates/server/src/test/resources/projects/enterprise/goal.txt new file mode 100644 index 00000000..8fdaa2de --- /dev/null +++ b/templates/server/src/test/resources/projects/enterprise/goal.txt @@ -0,0 +1 @@ +install -Pcomponenttest \ No newline at end of file