@@ -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 extends Annotation> 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 extends Annotation>... 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 extends Annotation>... annotations)
+ public final HibernateTemplateBuilder addTransformers(AnnotationTransformer extends Annotation>... 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 extends Annotation> 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 extends Annotation> annotation, List> allowed)
- {
- for (Class extends Annotation> 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 @@
+
+
+