diff --git a/.github/workflows/maven-build.yml b/.github/workflows/maven-build.yml index c8aeb3a..46e8e03 100644 --- a/.github/workflows/maven-build.yml +++ b/.github/workflows/maven-build.yml @@ -11,9 +11,7 @@ on: jobs: build: - runs-on: ubuntu-latest - steps: - uses: actions/checkout@v2 - name: Set up JDK 1.8 @@ -21,4 +19,6 @@ jobs: with: java-version: 1.8 - name: Build with Maven - run: mvn -B package --file pom.xml + run: mvn --batch-mode --update-snapshots clean package -DskipTests + - name: Unit tests + run: mvn --batch-mode --update-snapshots verify -DskipITs diff --git a/.github/workflows/maven_vulnerability.yml b/.github/workflows/maven_vulnerability.yml new file mode 100644 index 0000000..2b6deb5 --- /dev/null +++ b/.github/workflows/maven_vulnerability.yml @@ -0,0 +1,20 @@ +# This workflow will build a Java project with Maven +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven + +name: Java CI with Maven + +on: + push: + branches: [ master ] + +jobs: + vulnerability: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Vulnerability check + run: mvn org.owasp:dependency-check-maven:5.3.2:check -DfailBuildOnAnyVulnerability=true -DsuppressionFiles=suppression.xml diff --git a/pom.xml b/pom.xml index d92d1ef..add419f 100644 --- a/pom.xml +++ b/pom.xml @@ -4,14 +4,18 @@ de.intension halo-builder - 1.0.0-SNAPSHOT + 2.0.0-SNAPSHOT HALO Builder Java builder for the HATEOAS standard HALO + UTF-8 1.8 1.8 + + 5.4.2 + 1.4.2 @@ -44,9 +48,41 @@ 2.2 - org.hibernate + org.hibernate.validator hibernate-validator 6.1.5.Final + + org.junit.jupiter + junit-jupiter-api + ${org.junit.jupiter-version} + test + + + + org.junit.jupiter + junit-jupiter-engine + ${org.junit.jupiter-version} + test + + + + org.junit.platform + junit-platform-commons + ${org.junit.platform-version} + test + + + org.junit.platform + junit-platform-launcher + ${org.junit.platform-version} + test + + + org.hamcrest + hamcrest + 2.1 + test + \ No newline at end of file diff --git a/src/main/java/de/intension/halo/AnnotationTransformer.java b/src/main/java/de/intension/halo/AnnotationTransformer.java new file mode 100644 index 0000000..230f0b7 --- /dev/null +++ b/src/main/java/de/intension/halo/AnnotationTransformer.java @@ -0,0 +1,66 @@ +package de.intension.halo; + +import java.lang.annotation.Annotation; +import java.util.Map; + +import de.intension.halo.entity.Property; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +/** + * Transforms a property based on the given annotation. + *
+ *
+ * Implement this class with a simple constructor that calls sets the annotation type. + * + *
+ * public class ExampleTransformer 
+ *          extends AnnotationTransformer<ExampleAnnotation>
+ * {
+ *      public ExampleTransformer()
+ *      {
+ *          super(ExampleAnnotation.class);
+ *      }
+ * ...
+ * }
+ * 
+ */ +@RequiredArgsConstructor +public abstract class AnnotationTransformer +{ + + @NonNull + private Class annotationType; + + /** + * Check whether annotation class matches <T>. + * + * @param type Class of the annotation that is checked. + * @return true if annotation matches. + */ + public final boolean matchesAnnotation(Class type) + { + return annotationType.equals(type); + } + + /** + * Transform the property based on the information of an annotation. + * + * @param annotation Annotation to take information from. + * @param property Property to transform. + */ + public abstract void transformProperty(T annotation, Property property); + + /** + * Transform the property based on the information of an annotation and use localization. + * Override this method if localization is needed for the transformation. + * + * @param annotation Annotation to take information from. + * @param property Property to transform. + * @param locales Localization provider. + */ + public void transformProperty(T annotation, Property property, Map> locales) + { + transformProperty(annotation, property); + } +} diff --git a/src/main/java/de/intension/halo/entity/Property.java b/src/main/java/de/intension/halo/entity/Property.java index 8fe18dc..8c728b6 100644 --- a/src/main/java/de/intension/halo/entity/Property.java +++ b/src/main/java/de/intension/halo/entity/Property.java @@ -1,5 +1,6 @@ package de.intension.halo.entity; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -43,25 +44,12 @@ public class Property */ private Map title; /** - * Regular expression that restricts the {@link #value} of this property. - *
- *
- * In case of {@link #type} {@link DataType#DATE} this should contain the date format. + * List of validations to restrict the property value. * - * @param regex Set restriction to property value. - * @return Restriction in form of a regular expression. - */ - private String regex; - /** - * Defines weither this property is required on the object. - *
- *
- * A value of null should be interpreted as false. - * - * @param required Set weither this property is required. - * @return true | false | null. + * @param validations Set restrictions for this property. + * @return Restrictions of the property value. */ - private Boolean required; + private List validations; /** * Defines weither this property is write protected. *
@@ -134,4 +122,18 @@ public Property setTitle(String locale, String value) title.put(locale, value); return this; } + + /** + * Add validation to restrict the property value. + * + * @param validation Validation object. + */ + public Property addValidation(Validation validation) + { + if (validations == null) { + validations = new ArrayList<>(); + } + validations.add(validation); + return this; + } } diff --git a/src/main/java/de/intension/halo/entity/Validation.java b/src/main/java/de/intension/halo/entity/Validation.java new file mode 100644 index 0000000..ae2f6fb --- /dev/null +++ b/src/main/java/de/intension/halo/entity/Validation.java @@ -0,0 +1,35 @@ +package de.intension.halo.entity; + +import java.util.Map; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain = true) +@NoArgsConstructor +@RequiredArgsConstructor +public class Validation +{ + + /** + * + */ + @NonNull + private String name; + /** + * Validation object. + */ + @NonNull + private Object value; + /** + * Multi-language message of the validation. + * + * @param message Set mappings of language code to translation. + * @return Mappings of language code to translation. + */ + private Map message; +} diff --git a/src/main/java/de/intension/halo/hibernate/HibernateTemplateBuilder.java b/src/main/java/de/intension/halo/hibernate/HibernateTemplateBuilder.java index 50ed5f8..2249e3b 100644 --- a/src/main/java/de/intension/halo/hibernate/HibernateTemplateBuilder.java +++ b/src/main/java/de/intension/halo/hibernate/HibernateTemplateBuilder.java @@ -17,17 +17,13 @@ import java.util.Map; import java.util.logging.Level; -import javax.persistence.Id; -import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Pattern; - +import de.intension.halo.AnnotationTransformer; import de.intension.halo.entity.DataType; import de.intension.halo.entity.Link; import de.intension.halo.entity.Property; import de.intension.halo.entity.Template; +import de.intension.halo.entity.Validation; import lombok.NonNull; -import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.experimental.Accessors; import lombok.extern.java.Log; @@ -37,7 +33,6 @@ */ @Log @Accessors(chain = true) -@RequiredArgsConstructor public class HibernateTemplateBuilder { @@ -48,7 +43,7 @@ public class HibernateTemplateBuilder */ @Setter @NonNull - private String templateName; + private String templateName; /** * Mappings of property identifier to multi-language mapping. *
@@ -59,35 +54,55 @@ public class HibernateTemplateBuilder * @param locales Set mappings of property identifier to multi-language mapping. */ @Setter - private Map> locales; - private Map nestedLinks; - private List> readOnlyAnnotations = Arrays.asList(Id.class); - private List> requiredAnnotations = Arrays.asList(NotNull.class, NotEmpty.class); - private List> transientAnnotations = Arrays.asList(Transient.class); + private Map> locales; + /** + * List of annotation transformers. + * + * @param transformers Override list of annotation transformers. + */ + @Setter + private List> transformers; + private Map nestedLinks; + private List> transientAnnotations = Arrays.asList(Transient.class); /** - * Set annotations that define weither a property is write protected. + * Initialize a new template builder. + *
+ *
+ * By default the following annotation transformers are added. + * This behaviour can be overriden with {@link #setTransformers(List)}. + *
    + *
  • {@link RegexValidationMapper} + *
  • {@link LengthValidationMapper} + *
  • {@link RangeValidationMapper} + *
  • {@link IdTransformer} + *
  • {@link NotNullTransformer} + *
  • {@link NotEmptyTransformer} + *
* - * @param annotations Set annotations that define write protection. - * @return Annotations that define write protection. + * @param templateName Name of the template to build. */ - @SafeVarargs - public final HibernateTemplateBuilder setReadOnlyAnnotations(Class... annotations) + public HibernateTemplateBuilder(@NonNull String templateName) { - this.readOnlyAnnotations = Arrays.asList(annotations); - return this; + this.templateName = templateName; + addTransformers(new RegexValidationMapper(), new LengthValidationMapper(), new RangeValidationMapper(), + new IdTransformer(), new NotNullTransformer(), new NotEmptyTransformer()); } /** - * Set annotations that define weither a property is required. + * Add an annotation transformer to match for a certain annotation. * - * @param annotations Set annotations that define weither a property is required. - * @return Annotations that define weither a property is required. + * @param transformer Transformer to change property. */ @SafeVarargs - public final HibernateTemplateBuilder setRequiredAnnotations(Class... annotations) + public final HibernateTemplateBuilder addTransformers(AnnotationTransformer... transformers) { - this.requiredAnnotations = Arrays.asList(annotations); + if (this.transformers == null) { + this.transformers = new ArrayList<>(); + } + for (AnnotationTransformer transformer : transformers) { + this.transformers.add(transformer); + } return this; } @@ -175,6 +190,7 @@ private List buildProperties(Class entityType) return properties; } + @SuppressWarnings("unchecked") private Property buildProperty(Class entityType, Field field) { Property property = new Property(field.getName()); @@ -184,15 +200,10 @@ private Property buildProperty(Class entityType, Field field) setDataType(property, field.getGenericType()); for (Annotation annotation : field.getAnnotations()) { Class annotationType = annotation.annotationType(); - if (Pattern.class.equals(annotationType)) { - Pattern patternAnnotation = (Pattern)annotation; - property.setRegex(patternAnnotation.regexp()); - } - if (checkAnnotation(annotationType, requiredAnnotations)) { - property.setRequired(true); - } - if (checkAnnotation(annotationType, readOnlyAnnotations)) { - property.setReadOnly(true); + for (AnnotationTransformer transformer : transformers) { + if (transformer.matchesAnnotation(annotationType)) { + ((AnnotationTransformer)transformer).transformProperty(annotation, property, locales); + } } } return property; @@ -222,7 +233,7 @@ private void setDataType(Property property, Type type) } if (type.equals(LocalDateTime.class)) { property.setType(DataType.DATE); - property.setRegex("yyyy-MM-dd'T'HH:mm:ss.SSS"); + property.addValidation(new Validation("format", "yyyy-MM-dd'T'HH:mm:ss.SSS")); return; } property.setType(DataType.OBJECT); @@ -241,14 +252,4 @@ private boolean isTransient(Field field) } return false; } - - private boolean checkAnnotation(Class annotation, List> allowed) - { - for (Class a : allowed) { - if (a.equals(annotation)) { - return true; - } - } - return false; - } } diff --git a/src/main/java/de/intension/halo/hibernate/IdTransformer.java b/src/main/java/de/intension/halo/hibernate/IdTransformer.java new file mode 100644 index 0000000..31bbe8e --- /dev/null +++ b/src/main/java/de/intension/halo/hibernate/IdTransformer.java @@ -0,0 +1,25 @@ +package de.intension.halo.hibernate; + +import javax.persistence.Id; + +import de.intension.halo.AnnotationTransformer; +import de.intension.halo.entity.Property; + +/** + * Sets {@link Property#setReadOnly(Boolean)} to true for annotation {@link Id}. + */ +public class IdTransformer + extends AnnotationTransformer +{ + + public IdTransformer() + { + super(Id.class); + } + + @Override + public void transformProperty(Id annotation, Property property) + { + property.setReadOnly(true); + } +} diff --git a/src/main/java/de/intension/halo/hibernate/LengthValidationMapper.java b/src/main/java/de/intension/halo/hibernate/LengthValidationMapper.java new file mode 100644 index 0000000..ee8a614 --- /dev/null +++ b/src/main/java/de/intension/halo/hibernate/LengthValidationMapper.java @@ -0,0 +1,52 @@ +package de.intension.halo.hibernate; + +import java.util.Map; + +import org.hibernate.validator.constraints.Length; + +import de.intension.halo.AnnotationTransformer; +import de.intension.halo.entity.Property; +import de.intension.halo.entity.Validation; + +/** + * Maps {@link Length} annotation to 'minLength' and 'maxLength' validations. + */ +public class LengthValidationMapper + extends AnnotationTransformer + implements ValidationMessageDecorator +{ + + public LengthValidationMapper() + { + super(Length.class); + } + + @Override + public void transformProperty(Length annotation, Property property) + { + if (annotation.min() > 0) { + property.addValidation(new Validation("minLength", annotation.min())); + } + if (annotation.max() < Integer.MAX_VALUE) { + property.addValidation(new Validation("maxLength", annotation.max())); + } + } + + @Override + public void transformProperty(Length annotation, Property property, Map> locales) + { + Map message = getValidationMessage(annotation.message(), locales); + if (annotation.min() > 0) { + if (message != null) { + message.replaceAll((key, value) -> value.replace("{min}", String.valueOf(annotation.min()))); + } + property.addValidation(new Validation("minLength", annotation.min()).setMessage(message)); + } + if (annotation.max() < Integer.MAX_VALUE) { + if (message != null) { + message.replaceAll((key, value) -> value.replace("{max}", String.valueOf(annotation.max()))); + } + property.addValidation(new Validation("maxLength", annotation.max()).setMessage(message)); + } + } +} diff --git a/src/main/java/de/intension/halo/hibernate/NotEmptyTransformer.java b/src/main/java/de/intension/halo/hibernate/NotEmptyTransformer.java new file mode 100644 index 0000000..be4eb4c --- /dev/null +++ b/src/main/java/de/intension/halo/hibernate/NotEmptyTransformer.java @@ -0,0 +1,36 @@ +package de.intension.halo.hibernate; + +import java.util.Map; + +import javax.validation.constraints.NotEmpty; + +import de.intension.halo.AnnotationTransformer; +import de.intension.halo.entity.Property; +import de.intension.halo.entity.Validation; + +/** + * Sets {@link Property#setRequired(Boolean)} to true for annotation {@link NotEmpty}. + */ +public class NotEmptyTransformer + extends AnnotationTransformer + implements ValidationMessageDecorator +{ + + public NotEmptyTransformer() + { + super(NotEmpty.class); + } + + @Override + public void transformProperty(NotEmpty annotation, Property property) + { + property.addValidation(new Validation("required", true)); + } + + @Override + public void transformProperty(NotEmpty annotation, Property property, Map> locales) + { + property.addValidation(new Validation("required", true) + .setMessage(getValidationMessage(annotation.message(), locales))); + } +} diff --git a/src/main/java/de/intension/halo/hibernate/NotNullTransformer.java b/src/main/java/de/intension/halo/hibernate/NotNullTransformer.java new file mode 100644 index 0000000..02a42d0 --- /dev/null +++ b/src/main/java/de/intension/halo/hibernate/NotNullTransformer.java @@ -0,0 +1,36 @@ +package de.intension.halo.hibernate; + +import java.util.Map; + +import javax.validation.constraints.NotNull; + +import de.intension.halo.AnnotationTransformer; +import de.intension.halo.entity.Property; +import de.intension.halo.entity.Validation; + +/** + * Sets {@link Property#setRequired(Boolean)} to true for annotation {@link NotNull}. + */ +public class NotNullTransformer + extends AnnotationTransformer + implements ValidationMessageDecorator +{ + + public NotNullTransformer() + { + super(NotNull.class); + } + + @Override + public void transformProperty(NotNull annotation, Property property) + { + property.addValidation(new Validation("required", true)); + } + + @Override + public void transformProperty(NotNull annotation, Property property, Map> locales) + { + property.addValidation(new Validation("required", true) + .setMessage(getValidationMessage(annotation.message(), locales))); + } +} diff --git a/src/main/java/de/intension/halo/hibernate/RangeValidationMapper.java b/src/main/java/de/intension/halo/hibernate/RangeValidationMapper.java new file mode 100644 index 0000000..8acec08 --- /dev/null +++ b/src/main/java/de/intension/halo/hibernate/RangeValidationMapper.java @@ -0,0 +1,52 @@ +package de.intension.halo.hibernate; + +import java.util.Map; + +import org.hibernate.validator.constraints.Range; + +import de.intension.halo.AnnotationTransformer; +import de.intension.halo.entity.Property; +import de.intension.halo.entity.Validation; + +/** + * Maps {@link Range} annotation to 'minValue' and 'maxValue' validations. + */ +public class RangeValidationMapper + extends AnnotationTransformer + implements ValidationMessageDecorator +{ + + public RangeValidationMapper() + { + super(Range.class); + } + + @Override + public void transformProperty(Range annotation, Property property) + { + if (annotation.min() > 0) { + property.addValidation(new Validation("minValue", annotation.min())); + } + if (annotation.max() < Long.MAX_VALUE) { + property.addValidation(new Validation("maxValue", annotation.max())); + } + } + + @Override + public void transformProperty(Range annotation, Property property, Map> locales) + { + Map message = getValidationMessage(annotation.message(), locales); + if (annotation.min() > 0) { + if (message != null) { + message.replaceAll((key, value) -> value.replace("{min}", String.valueOf(annotation.min()))); + } + property.addValidation(new Validation("minValue", annotation.min()).setMessage(message)); + } + if (annotation.max() < Long.MAX_VALUE) { + if (message != null) { + message.replaceAll((key, value) -> value.replace("{max}", String.valueOf(annotation.max()))); + } + property.addValidation(new Validation("maxValue", annotation.max()).setMessage(message)); + } + } +} diff --git a/src/main/java/de/intension/halo/hibernate/RegexValidationMapper.java b/src/main/java/de/intension/halo/hibernate/RegexValidationMapper.java new file mode 100644 index 0000000..f2e85fc --- /dev/null +++ b/src/main/java/de/intension/halo/hibernate/RegexValidationMapper.java @@ -0,0 +1,36 @@ +package de.intension.halo.hibernate; + +import java.util.Map; + +import javax.validation.constraints.Pattern; + +import de.intension.halo.AnnotationTransformer; +import de.intension.halo.entity.Property; +import de.intension.halo.entity.Validation; + +/** + * Maps {@link Pattern} annotation to 'regex' validation. + */ +public class RegexValidationMapper + extends AnnotationTransformer + implements ValidationMessageDecorator +{ + + public RegexValidationMapper() + { + super(Pattern.class); + } + + @Override + public void transformProperty(Pattern annotation, Property property) + { + property.addValidation(new Validation("regex", annotation.regexp())); + } + + @Override + public void transformProperty(Pattern annotation, Property property, Map> locales) + { + property.addValidation(new Validation("regex", annotation.regexp()) + .setMessage(getValidationMessage(annotation.message(), locales))); + } +} diff --git a/src/main/java/de/intension/halo/hibernate/ValidationMessageDecorator.java b/src/main/java/de/intension/halo/hibernate/ValidationMessageDecorator.java new file mode 100644 index 0000000..642283f --- /dev/null +++ b/src/main/java/de/intension/halo/hibernate/ValidationMessageDecorator.java @@ -0,0 +1,29 @@ +package de.intension.halo.hibernate; + +import java.util.Map; + +/** + * Provides utility methods for validation message processing. + */ +public interface ValidationMessageDecorator +{ + + /** + * Get multi-language message from map with identifier like: + * + *
+     * {Example.message}
+     * 
+ * + * @param message Message that may be surrounded by curly braces. + * @param locales Map of identifier to multi-language message. + * @return Multi-language message or null if it cannot be found. + */ + default Map getValidationMessage(String message, Map> locales) + { + if (message == null || locales == null) { + return null; + } + return locales.get(message.replaceAll("^\\{|\\}$", "")); + } +} diff --git a/src/test/java/de/intension/halo/hibernate/HibernateDecoratorTest.java b/src/test/java/de/intension/halo/hibernate/HibernateDecoratorTest.java new file mode 100644 index 0000000..31a8f7c --- /dev/null +++ b/src/test/java/de/intension/halo/hibernate/HibernateDecoratorTest.java @@ -0,0 +1,74 @@ +package de.intension.halo.hibernate; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +class HibernateDecoratorTest + implements ValidationMessageDecorator +{ + + @Test + void should_find_message_with_curly_braces() + { + Map> locales = new HashMap<>(); + locales.put("Test.value", testValue()); + + Map message = getValidationMessage("{Test.value}", locales); + + assertThat(message, notNullValue()); + } + + @Test + void should_find_message_without_curly_braces() + { + Map> locales = new HashMap<>(); + locales.put("Test.value", testValue()); + + Map message = getValidationMessage("Test.value", locales); + + assertThat(message, notNullValue()); + } + + @Test + void should_return_null_if_message_not_found() + { + Map> locales = new HashMap<>(); + locales.put("Test.value", testValue()); + + Map message = getValidationMessage("nothing", locales); + + assertThat(message, nullValue()); + } + + @Test + void should_return_null_if_key_is_null() + { + Map> locales = new HashMap<>(); + locales.put("Test.value", testValue()); + + Map message = getValidationMessage(null, locales); + + assertThat(message, nullValue()); + } + + @Test + void should_return_null_if_map_is_null() + { + Map message = getValidationMessage("Test.value", null); + + assertThat(message, nullValue()); + } + + private static Map testValue() + { + Map value = new HashMap<>(); + value.put("en", "test"); + return value; + } +} diff --git a/suppression.xml b/suppression.xml new file mode 100644 index 0000000..3af0cdf --- /dev/null +++ b/suppression.xml @@ -0,0 +1,3 @@ + + +