From 1c7646f4800537e84739d7d1811baf3fd11106a2 Mon Sep 17 00:00:00 2001 From: JanHolger Date: Sun, 3 Sep 2023 20:25:35 +0200 Subject: [PATCH 01/19] Incremented version number --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 401f5c6..cb466c1 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ 8 8 - 1.0.6-SNAPSHOT + 1.0.7-SNAPSHOT org.javawebstack From 6f1b31e931f2a30fb502dead75a25283493f5f63 Mon Sep 17 00:00:00 2001 From: JanHolger Date: Mon, 4 Sep 2023 02:29:16 +0200 Subject: [PATCH 02/19] Incremented version number --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 021e4ff..4ca0d32 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,6 @@ You can find the current docs on our [website](https://docs.javawebstack.org/fra org.javawebstack abstract-data - 1.0.6 + 1.0.7 ``` From 2e927fefda8621a18b91580e21676ae58a345579 Mon Sep 17 00:00:00 2001 From: JanHolger Date: Thu, 2 Nov 2023 06:32:34 +0100 Subject: [PATCH 03/19] Implemented schema validation tools --- .../abstractdata/AbstractPath.java | 97 ++++++++++++++++ .../schema/AbstractArraySchema.java | 92 +++++++++++++++ .../schema/AbstractBooleanSchema.java | 49 ++++++++ .../schema/AbstractNumberSchema.java | 85 ++++++++++++++ .../schema/AbstractObjectSchema.java | 79 +++++++++++++ .../abstractdata/schema/AbstractSchema.java | 72 ++++++++++++ .../schema/AbstractStringSchema.java | 107 ++++++++++++++++++ .../abstractdata/schema/CustomValidation.java | 12 ++ .../abstractdata/schema/OneOfSchema.java | 35 ++++++ .../schema/SchemaValidationError.java | 80 +++++++++++++ 10 files changed, 708 insertions(+) create mode 100644 src/main/java/org/javawebstack/abstractdata/AbstractPath.java create mode 100644 src/main/java/org/javawebstack/abstractdata/schema/AbstractArraySchema.java create mode 100644 src/main/java/org/javawebstack/abstractdata/schema/AbstractBooleanSchema.java create mode 100644 src/main/java/org/javawebstack/abstractdata/schema/AbstractNumberSchema.java create mode 100644 src/main/java/org/javawebstack/abstractdata/schema/AbstractObjectSchema.java create mode 100644 src/main/java/org/javawebstack/abstractdata/schema/AbstractSchema.java create mode 100644 src/main/java/org/javawebstack/abstractdata/schema/AbstractStringSchema.java create mode 100644 src/main/java/org/javawebstack/abstractdata/schema/CustomValidation.java create mode 100644 src/main/java/org/javawebstack/abstractdata/schema/OneOfSchema.java create mode 100644 src/main/java/org/javawebstack/abstractdata/schema/SchemaValidationError.java diff --git a/src/main/java/org/javawebstack/abstractdata/AbstractPath.java b/src/main/java/org/javawebstack/abstractdata/AbstractPath.java new file mode 100644 index 0000000..e64640e --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/AbstractPath.java @@ -0,0 +1,97 @@ +package org.javawebstack.abstractdata; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class AbstractPath { + + public static final AbstractPath ROOT = new AbstractPath(null, null); + + private final AbstractPath parent; + private final String name; + + public AbstractPath(String name) { + this(ROOT, name); + if(name == null || name.isEmpty()) + throw new IllegalArgumentException("Name can not be null or empty"); + } + + private AbstractPath(AbstractPath parent, String name) { + this.parent = parent; + this.name = name; + } + + public String getName() { + return name; + } + + public AbstractPath getParent() { + return parent; + } + + public AbstractPath subPath(String name) { + return new AbstractPath(this, name); + } + + public AbstractPath clone() { + return new AbstractPath( + this.parent != null ? this.parent.clone() : null, + name + ); + } + + public AbstractPath concat(AbstractPath path) { + AbstractPath cloned = clone(); + for(String part : path.getParts()) + cloned = cloned.subPath(part); + return cloned; + } + + public List getParts() { + List parts = parent != null ? parent.getParts() : new ArrayList<>(); + if(name != null) + parts.add(name); + return parts; + } + + public String toString() { + return String.join(".", getParts()); + } + + public static AbstractPath parse(String s) { + s = s.trim(); + if(s.isEmpty()) + return ROOT; + String[] spl = s.split("\\."); + AbstractPath path = new AbstractPath(spl[0]); + for(int i=1; i> customValidations = new ArrayList<>(); + + public AbstractArraySchema itemSchema(AbstractSchema schema) { + this.itemSchema = schema; + return this; + } + + public AbstractArraySchema min(int min) { + this.min = min; + return this; + } + + public AbstractArraySchema max(int max) { + this.max = max; + return this; + } + + public AbstractArraySchema allowNull() { + this.allowNull = true; + return this; + } + + public AbstractArraySchema customValidation(CustomValidation validation) { + customValidations.add(validation); + return this; + } + + public AbstractSchema getItemSchema() { + return itemSchema; + } + + public Integer getMin() { + return min; + } + + public Integer getMax() { + return max; + } + + public List> getCustomValidations() { + return customValidations; + } + + public List validate(AbstractPath path, AbstractElement value) { + List errors = new ArrayList<>(); + if(value.getType() != AbstractElement.Type.ARRAY) { + errors.add(new SchemaValidationError(path, "invalid_type").meta("expected", "array").meta("actual", value.getType().name().toLowerCase(Locale.ROOT))); + return errors; + } + AbstractArray array = value.array(); + if(min != null && array.size() < min) { + errors.add(new SchemaValidationError(path, "not_enough_items").meta("min", String.valueOf(min)).meta("actual", String.valueOf(array.size()))); + } + if(max != null && array.size() > max) { + errors.add(new SchemaValidationError(path, "too_many_items").meta("max", String.valueOf(max)).meta("actual", String.valueOf(array.size()))); + } + if(itemSchema != null) { + for(int i=0; i validation : customValidations) { + errors.addAll(validation.validate(path, array)); + } + return errors; + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/schema/AbstractBooleanSchema.java b/src/main/java/org/javawebstack/abstractdata/schema/AbstractBooleanSchema.java new file mode 100644 index 0000000..958e1a9 --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/schema/AbstractBooleanSchema.java @@ -0,0 +1,49 @@ +package org.javawebstack.abstractdata.schema; + +import org.javawebstack.abstractdata.AbstractElement; +import org.javawebstack.abstractdata.AbstractPath; +import org.javawebstack.abstractdata.AbstractPrimitive; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class AbstractBooleanSchema implements AbstractSchema { + + private Boolean staticValue; + private final List> customValidations = new ArrayList<>(); + + public AbstractBooleanSchema staticValue(boolean value) { + this.staticValue = value; + return this; + } + + public AbstractBooleanSchema customValidation(CustomValidation validation) { + customValidations.add(validation); + return this; + } + + public Boolean getStaticValue() { + return staticValue; + } + + public List> getCustomValidations() { + return customValidations; + } + + public List validate(AbstractPath path, AbstractElement value) { + List errors = new ArrayList<>(); + if(value.getType() != AbstractElement.Type.BOOLEAN) { + errors.add(new SchemaValidationError(path, "invalid_type").meta("expected", "boolean").meta("actual", value.getType().name().toLowerCase(Locale.ROOT))); + return errors; + } + if(staticValue != null && staticValue != value.bool()) { + errors.add(new SchemaValidationError(path, "invalid_static_value").meta("expected", staticValue.toString()).meta("actual", value.bool().toString())); + } + for(CustomValidation validation : customValidations) { + errors.addAll(validation.validate(path, value.primitive())); + } + return errors; + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/schema/AbstractNumberSchema.java b/src/main/java/org/javawebstack/abstractdata/schema/AbstractNumberSchema.java new file mode 100644 index 0000000..8561c6c --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/schema/AbstractNumberSchema.java @@ -0,0 +1,85 @@ +package org.javawebstack.abstractdata.schema; + +import org.javawebstack.abstractdata.AbstractElement; +import org.javawebstack.abstractdata.AbstractPath; +import org.javawebstack.abstractdata.AbstractPrimitive; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class AbstractNumberSchema implements AbstractSchema { + + private boolean integerOnly = false; + private Number min; + private Number max; + private final List> customValidations = new ArrayList<>(); + + public AbstractNumberSchema min(Number min) { + this.min = min; + return this; + } + + public AbstractNumberSchema max(Number max) { + this.max = max; + return this; + } + + public AbstractNumberSchema integerOnly() { + this.integerOnly = true; + return this; + } + + public AbstractNumberSchema customValidation(CustomValidation validation) { + customValidations.add(validation); + return this; + } + + public Number getMin() { + return min; + } + + public Number getMax() { + return max; + } + + public boolean isIntegerOnly() { + return integerOnly; + } + + public List> getCustomValidations() { + return customValidations; + } + + public List validate(AbstractPath path, AbstractElement value) { + List errors = new ArrayList<>(); + if(value.getType() != AbstractElement.Type.NUMBER) { + errors.add(new SchemaValidationError(path, "invalid_type").meta("expected", integerOnly ? "integer" : "number").meta("actual", value.getType().name().toLowerCase(Locale.ROOT))); + return errors; + } + Number n = value.number(); + BigDecimal dN = (n instanceof Float || n instanceof Double) ? BigDecimal.valueOf(n.doubleValue()) : BigDecimal.valueOf(n.longValue()); + if(integerOnly && (n instanceof Float || n instanceof Double)) { + errors.add(new SchemaValidationError(path, "invalid_type").meta("expected", "integer").meta("actual", "number")); + return errors; + } + if(min != null) { + BigDecimal dMin = (min instanceof Float || min instanceof Double) ? BigDecimal.valueOf(min.doubleValue()) : BigDecimal.valueOf(min.longValue()); + if(dN.compareTo(dMin) < 0) { + errors.add(new SchemaValidationError(path, "number_smaller_than_min").meta("min", dMin.toPlainString()).meta("actual", dN.toPlainString())); + } + } + if(max != null) { + BigDecimal dMax = (max instanceof Float || min instanceof Double) ? BigDecimal.valueOf(max.doubleValue()) : BigDecimal.valueOf(max.longValue()); + if(dN.compareTo(dMax) > 0) { + errors.add(new SchemaValidationError(path, "number_larger_than_max").meta("max", dMax.toPlainString()).meta("actual", dN.toPlainString())); + } + } + for(CustomValidation validation : customValidations) { + errors.addAll(validation.validate(path, value.primitive())); + } + return errors; + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/schema/AbstractObjectSchema.java b/src/main/java/org/javawebstack/abstractdata/schema/AbstractObjectSchema.java new file mode 100644 index 0000000..f3c4eaf --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/schema/AbstractObjectSchema.java @@ -0,0 +1,79 @@ +package org.javawebstack.abstractdata.schema; + +import org.javawebstack.abstractdata.AbstractElement; +import org.javawebstack.abstractdata.AbstractObject; +import org.javawebstack.abstractdata.AbstractPath; + +import java.util.*; + +public class AbstractObjectSchema implements AbstractSchema { + + private final Map properties = new HashMap<>(); + private final Set requiredProperties = new HashSet<>(); + private final List> customValidations = new ArrayList<>(); + private boolean allowAdditionalProperties = false; + private AbstractSchema additionalPropertySchema; + + public AbstractObjectSchema requiredProperty(String name, AbstractSchema schema) { + properties.put(name, schema); + requiredProperties.add(name); + return this; + } + + public AbstractObjectSchema optionalProperty(String name, AbstractSchema schema) { + properties.put(name, schema); + requiredProperties.remove(name); + return this; + } + + public AbstractObjectSchema customValidation(CustomValidation validation) { + customValidations.add(validation); + return this; + } + + public AbstractObjectSchema additionalProperties() { + return additionalProperties(null); + } + + public AbstractObjectSchema additionalProperties(AbstractSchema schema) { + allowAdditionalProperties = true; + additionalPropertySchema = schema; + return this; + } + + public List validate(AbstractPath path, AbstractElement value) { + List errors = new ArrayList<>(); + if(value.getType() != AbstractElement.Type.OBJECT) { + errors.add(new SchemaValidationError(path, "invalid_type").meta("expected", "object").meta("actual", value.getType().name().toLowerCase(Locale.ROOT))); + return errors; + } + AbstractObject object = value.object(); + for(String prop : requiredProperties) { + if(!object.has(prop) || object.get(prop).isNull()) { + errors.add(new SchemaValidationError(path.subPath(prop), "missing_required_property")); + } + } + for(String prop : object.keys()) { + AbstractElement propValue = object.get(prop); + AbstractPath propPath = path.subPath(prop); + if(properties.containsKey(prop)) { + if(propValue.isNull()) + continue; + errors.addAll(properties.get(prop).validate(propPath, propValue)); + } else { + if(allowAdditionalProperties) { + if(additionalPropertySchema != null) { + errors.addAll(additionalPropertySchema.validate(propPath, propValue)); + } + } else { + errors.add(new SchemaValidationError(propPath, "unexpected_property")); + } + } + } + for(CustomValidation validation : customValidations) { + errors.addAll(validation.validate(path, object)); + } + return errors; + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/schema/AbstractSchema.java b/src/main/java/org/javawebstack/abstractdata/schema/AbstractSchema.java new file mode 100644 index 0000000..8b06d54 --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/schema/AbstractSchema.java @@ -0,0 +1,72 @@ +package org.javawebstack.abstractdata.schema; + +import org.javawebstack.abstractdata.AbstractElement; +import org.javawebstack.abstractdata.AbstractPath; + +import java.util.List; + +public interface AbstractSchema { + + default List validate(AbstractElement value) { + return validate(AbstractPath.ROOT, value); + } + + List validate(AbstractPath path, AbstractElement value); + + static AbstractArraySchema array(AbstractSchema itemSchema) { + return array().itemSchema(itemSchema); + } + + static AbstractArraySchema array() { + return new AbstractArraySchema(); + } + + static AbstractObjectSchema object() { + return new AbstractObjectSchema(); + } + + static AbstractStringSchema staticString(String s) { + return string().staticValue(s); + } + + static AbstractStringSchema enumString(Class> enumType) { + return string().enumValues(enumType); + } + + static AbstractStringSchema enumString(String... values) { + return string().enumValues(values); + } + + static AbstractStringSchema string() { + return new AbstractStringSchema(); + } + + static AbstractNumberSchema integer(int min, int max) { + return integer().min(min).max(max); + } + + static AbstractNumberSchema integer() { + return new AbstractNumberSchema().integerOnly(); + } + + static AbstractNumberSchema number() { + return new AbstractNumberSchema(); + } + + static AbstractNumberSchema number(double min, double max) { + return number().min(min).max(max); + } + + static AbstractBooleanSchema staticBool(boolean v) { + return bool().staticValue(v); + } + + static AbstractBooleanSchema bool() { + return new AbstractBooleanSchema(); + } + + static OneOfSchema oneOf(AbstractSchema... schemas) { + return new OneOfSchema(schemas); + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/schema/AbstractStringSchema.java b/src/main/java/org/javawebstack/abstractdata/schema/AbstractStringSchema.java new file mode 100644 index 0000000..10f6f46 --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/schema/AbstractStringSchema.java @@ -0,0 +1,107 @@ +package org.javawebstack.abstractdata.schema; + +import org.javawebstack.abstractdata.AbstractElement; +import org.javawebstack.abstractdata.AbstractPath; +import org.javawebstack.abstractdata.AbstractPrimitive; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class AbstractStringSchema implements AbstractSchema { + + private Integer minLength; + private Integer maxLength; + private String staticValue; + private String regex; + private Pattern regexPattern; + private Set enumValues; + private final List> customValidations = new ArrayList<>(); + + public AbstractStringSchema staticValue(String value) { + this.staticValue = value; + return this; + } + + public AbstractStringSchema enumValues(Class> enumType) { + Set values = new HashSet<>(); + for(Enum v : enumType.getEnumConstants()) { + values.add(v.name()); + } + return enumValues(values); + } + + public AbstractStringSchema enumValues(String... values) { + return enumValues(new HashSet<>(Arrays.asList(values))); + } + + public AbstractStringSchema enumValues(Set values) { + this.enumValues = values; + return this; + } + + public AbstractStringSchema minLength(int min) { + this.minLength = min; + return this; + } + + public AbstractStringSchema maxLength(int max) { + this.maxLength = max; + return this; + } + + public AbstractStringSchema regex(String regex) { + this.regex = regex; + this.regexPattern = Pattern.compile(regex); + return this; + } + + public AbstractStringSchema customValidation(CustomValidation validation) { + customValidations.add(validation); + return this; + } + + public String getRegex() { + return regex; + } + + public String getStaticValue() { + return staticValue; + } + + public List> getCustomValidations() { + return customValidations; + } + + public List validate(AbstractPath path, AbstractElement value) { + List errors = new ArrayList<>(); + if(value.getType() != AbstractElement.Type.STRING) { + errors.add(new SchemaValidationError(path, "invalid_type").meta("expected", "string").meta("actual", value.getType().name().toLowerCase(Locale.ROOT))); + return errors; + } + String s = value.string(); + if(staticValue != null && !staticValue.equals(s)) { + errors.add(new SchemaValidationError(path, "invalid_static_value").meta("expected", staticValue).meta("actual", s)); + } + if(enumValues != null && !enumValues.contains(s)) { + errors.add(new SchemaValidationError(path, "invalid_enum_value").meta("expected", String.join(", ", enumValues)).meta("actual", s)); + } + if(minLength != null && s.length() < minLength) { + errors.add(new SchemaValidationError(path, "value_too_short").meta("min", minLength.toString()).meta("actual", String.valueOf(s.length()))); + } + if(maxLength != null && s.length() > maxLength) { + errors.add(new SchemaValidationError(path, "value_too_long").meta("max", maxLength.toString()).meta("actual", String.valueOf(s.length()))); + } + if(regexPattern != null) { + Matcher matcher = regexPattern.matcher(s); + if(!matcher.matches()) { + errors.add(new SchemaValidationError(path, "invalid_pattern").meta("pattern", regex).meta("actual", s)); + } + } + for(CustomValidation validation : customValidations) { + errors.addAll(validation.validate(path, value.primitive())); + } + return errors; + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/schema/CustomValidation.java b/src/main/java/org/javawebstack/abstractdata/schema/CustomValidation.java new file mode 100644 index 0000000..1001f0d --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/schema/CustomValidation.java @@ -0,0 +1,12 @@ +package org.javawebstack.abstractdata.schema; + +import org.javawebstack.abstractdata.AbstractElement; +import org.javawebstack.abstractdata.AbstractPath; + +import java.util.List; + +public interface CustomValidation { + + List validate(AbstractPath path, T value); + +} diff --git a/src/main/java/org/javawebstack/abstractdata/schema/OneOfSchema.java b/src/main/java/org/javawebstack/abstractdata/schema/OneOfSchema.java new file mode 100644 index 0000000..4960632 --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/schema/OneOfSchema.java @@ -0,0 +1,35 @@ +package org.javawebstack.abstractdata.schema; + +import org.javawebstack.abstractdata.AbstractElement; +import org.javawebstack.abstractdata.AbstractPath; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class OneOfSchema implements AbstractSchema { + + private final List schemas = new ArrayList<>(); + + public OneOfSchema(AbstractSchema... schemas) { + if(schemas.length == 0) + throw new IllegalArgumentException("At least one schema is required"); + this.schemas.addAll(Arrays.asList(schemas)); + } + + public List validate(AbstractPath path, AbstractElement value) { + List> schemaErrors = new ArrayList<>(); + for(AbstractSchema schema : schemas) { + List errors = schema.validate(path, value); + if(errors.isEmpty()) + return errors; + schemaErrors.add(errors); + } + for(List errors : schemaErrors) { + if(!(errors.size() == 1 && errors.get(0).getError().equals("invalid_type"))) + return errors; + } + return schemaErrors.get(0); + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/schema/SchemaValidationError.java b/src/main/java/org/javawebstack/abstractdata/schema/SchemaValidationError.java new file mode 100644 index 0000000..f516f39 --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/schema/SchemaValidationError.java @@ -0,0 +1,80 @@ +package org.javawebstack.abstractdata.schema; + +import org.javawebstack.abstractdata.AbstractPath; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class SchemaValidationError { + + private static final Map BUILTIN_DESCRIPTIONS = new HashMap() {{ + put("invalid_type", "Expected value of type {expected}, got {actual}"); + put("not_enough_items", "At least {min} item(s) required, got {actual}"); + put("too_many_items", "Not more than {max} item(s) allowed, got {actual}"); + put("null_not_allowed", "Value null is not allowed"); + put("invalid_static_value", "Static value does not match, expected '{expected}' but got '{actual}'"); + put("number_smaller_than_min", "The value '{actual}' is smaller than {min}"); + put("number_larger_than_max", "The value '{actual}' is larger than {max}"); + put("missing_required_property", "The property is required but missing"); + put("unexpected_property", "Unexpected property, additional properties are not allowed"); + put("value_too_short", "The length of the value ({actual}) is shorter than the minimum of {min}"); + put("value_too_long", "The length of the value ({actual}) is longer than the maximum of {max}"); + put("invalid_pattern", "The value '{actual}' does not match the pattern '{pattern}'"); + }}; + + private AbstractPath path; + private String error; + private Map errorMeta = new HashMap<>(); + + public SchemaValidationError(AbstractPath path, String error) { + this.path = path; + this.error = error; + } + + public SchemaValidationError meta(String key, String value) { + errorMeta.put(key, value); + return this; + } + + public AbstractPath getPath() { + return path; + } + + public String getError() { + return error; + } + + public Map getErrorMeta() { + return errorMeta; + } + + public String getErrorDescription() { + return getErrorDescription(new HashMap<>()); + } + + public String getErrorDescription(Map customDescriptions) { + String message; + if(customDescriptions.containsKey(error)) { + message = customDescriptions.get(error); + } else if(BUILTIN_DESCRIPTIONS.containsKey(error)) { + message = BUILTIN_DESCRIPTIONS.get(error); + } else { + return error; + } + for(String key : errorMeta.keySet()) { + message = message.replace("{" + key + "}", errorMeta.get(key)); + } + return message; + } + + public static Map> groupErrors(List errors) { + Map> errorMap = new HashMap<>(); + for(SchemaValidationError e : errors) { + errorMap.computeIfAbsent(e.getPath(), k -> new ArrayList<>()).add(e); + } + return errorMap; + } + +} From e3de5aab40b128614b5ee310e74b76d1094ce014 Mon Sep 17 00:00:00 2001 From: Jake Esser Date: Thu, 9 Nov 2023 13:37:30 +0100 Subject: [PATCH 04/19] Fxied DateFormat annotation issue --- .../javawebstack/abstractdata/mapper/DefaultMappers.java | 2 +- .../abstractdata/mapper/annotation/DateFormat.java | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/javawebstack/abstractdata/mapper/DefaultMappers.java b/src/main/java/org/javawebstack/abstractdata/mapper/DefaultMappers.java index 2ed0b60..0a7a682 100644 --- a/src/main/java/org/javawebstack/abstractdata/mapper/DefaultMappers.java +++ b/src/main/java/org/javawebstack/abstractdata/mapper/DefaultMappers.java @@ -263,7 +263,7 @@ public AbstractElement toAbstract(MapperContext context, Object value) throws Ma public Object fromAbstract(MapperContext context, AbstractElement element, Class type) throws MapperException { try { - DateFormat df = context.getAnnotation(DateFormat.class); + DateFormat df = context.getField().getAnnotation(DateFormat.class); Date date; if (df != null && df.epoch()) { long time = element.number(context.getMapper().isStrict()).longValue(); diff --git a/src/main/java/org/javawebstack/abstractdata/mapper/annotation/DateFormat.java b/src/main/java/org/javawebstack/abstractdata/mapper/annotation/DateFormat.java index bcedb86..d752599 100644 --- a/src/main/java/org/javawebstack/abstractdata/mapper/annotation/DateFormat.java +++ b/src/main/java/org/javawebstack/abstractdata/mapper/annotation/DateFormat.java @@ -1,5 +1,12 @@ package org.javawebstack.abstractdata.mapper.annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) public @interface DateFormat { String value() default ""; From b827750fb648b3cd2c341ad3513afeaf8380420d Mon Sep 17 00:00:00 2001 From: JanHolger Date: Thu, 9 Nov 2023 23:17:43 +0100 Subject: [PATCH 05/19] Changed the gpg key id --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cb466c1..891d0d3 100644 --- a/pom.xml +++ b/pom.xml @@ -113,7 +113,7 @@ sign - A313520526A8DFE1C2A30399C35A3D43C557B112 + 6802C125C9069802B97A080910CE6063CEE9C47B gpg --no-tty From e65f99e5abe4931b4bad369cfeb3a3a069f1505f Mon Sep 17 00:00:00 2001 From: JanHolger Date: Tue, 19 Dec 2023 16:15:52 +0100 Subject: [PATCH 06/19] Started to work on a parser for json schema --- pom.xml | 2 +- .../abstractdata/schema/AbstractSchema.java | 5 + .../abstractdata/schema/JsonSchemaParser.java | 158 ++++++++++++++++++ 3 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/javawebstack/abstractdata/schema/JsonSchemaParser.java diff --git a/pom.xml b/pom.xml index 891d0d3..e157e32 100644 --- a/pom.xml +++ b/pom.xml @@ -49,7 +49,7 @@ org.mongodb bson - 4.10.2 + 4.11.1 true diff --git a/src/main/java/org/javawebstack/abstractdata/schema/AbstractSchema.java b/src/main/java/org/javawebstack/abstractdata/schema/AbstractSchema.java index 8b06d54..fa4acd0 100644 --- a/src/main/java/org/javawebstack/abstractdata/schema/AbstractSchema.java +++ b/src/main/java/org/javawebstack/abstractdata/schema/AbstractSchema.java @@ -1,6 +1,7 @@ package org.javawebstack.abstractdata.schema; import org.javawebstack.abstractdata.AbstractElement; +import org.javawebstack.abstractdata.AbstractObject; import org.javawebstack.abstractdata.AbstractPath; import java.util.List; @@ -69,4 +70,8 @@ static OneOfSchema oneOf(AbstractSchema... schemas) { return new OneOfSchema(schemas); } + static AbstractSchema fromJsonSchema(AbstractObject schema) { + return new JsonSchemaParser().parse(schema); + } + } diff --git a/src/main/java/org/javawebstack/abstractdata/schema/JsonSchemaParser.java b/src/main/java/org/javawebstack/abstractdata/schema/JsonSchemaParser.java new file mode 100644 index 0000000..3af934e --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/schema/JsonSchemaParser.java @@ -0,0 +1,158 @@ +package org.javawebstack.abstractdata.schema; + +import org.javawebstack.abstractdata.AbstractArray; +import org.javawebstack.abstractdata.AbstractElement; +import org.javawebstack.abstractdata.AbstractObject; + +import java.util.Collections; +import java.util.List; + +/* +{ + "$id": "1224", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Person", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "emails": { + "type": "array" + "items": { + "type": "string" + } + }, + "age": { + "type": "integer", + "minimum": 0, + "maximum": 120 + } + }, + "required": [ + "name", + "emails" + ] +} + */ + +public class JsonSchemaParser { + + public static void main(String[] args) { + String schemaStr = "{\n" + + " \"$id\": \"1224\",\n" + + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n" + + " \"title\": \"Person\",\n" + + " \"type\": \"object\",\n" + + " \"properties\": {\n" + + " \"name\": {\n" + + " \"type\": \"string\"\n" + + " },\n" + + " \"emails\": {\n" + + " \"type\": \"array\"\n" + + " \"minItems\": 1\n" + + " \"items\": {\n" + + " \"type\": \"string\"\n" + + " }\n" + + " },\n" + + " \"age\": {\n" + + " \"type\": \"integer\",\n" + + " \"minimum\": 0,\n" + + " \"maximum\": 120\n" + + " }\n" + + " },\n" + + " \"required\": [\n" + + " \"name\",\n" + + " \"emails\"\n" + + " ]\n" + + "}"; + AbstractObject schemaObj = AbstractElement.fromJson(schemaStr).object(); + AbstractSchema schema = new JsonSchemaParser().parse(schemaObj); + List errors = schema.validate(new AbstractObject() + .set("name", "Maher") + .set("emails", new AbstractArray()) + .set("age", 123) + ); + System.out.println(); + + } + + public AbstractSchema parse(AbstractObject schema) { + if(schema.has("type")) { + switch (schema.string("type")) { + case "object": { + return parseObject(schema); + } + case "array": { + return parseArray(schema); + } + case "string": { + return parseString(schema); + } + case "integer": { + return parseInteger(schema); + } + default: { + throw new UnsupportedOperationException("Unknown type: " + schema.string("type")); + } + } + } + if(schema.has("$ref")) { + throw new UnsupportedOperationException("$ref is currently not supported"); + } + throw new IllegalArgumentException("Invalid json schema"); + } + + private AbstractStringSchema parseString(AbstractObject schema) { + if(!schema.string("type").equals("string")) + throw new IllegalArgumentException("Not a valid string schema"); + AbstractStringSchema s = new AbstractStringSchema(); + + return s; + } + + private AbstractNumberSchema parseInteger(AbstractObject schema) { + if(!schema.string("type").equals("integer")) + throw new IllegalArgumentException("Not a valid integer schema"); + AbstractNumberSchema s = new AbstractNumberSchema(); + if(schema.has("minimum")) + s.min(schema.number("minimum")); + if(schema.has("maximum")) + s.max(schema.number("maximum")); + return s; + } + + private AbstractObjectSchema parseObject(AbstractObject schema) { + if(!schema.string("type").equals("object")) + throw new IllegalArgumentException("Not a valid object schema"); + AbstractObjectSchema s = new AbstractObjectSchema(); + List required = schema.has("required") ? schema.array("required").toStringList() : Collections.emptyList(); + if(schema.has("properties")) { + schema.object("properties").forEach((key, propSchema) -> { + if(required.contains(key)) { + s.requiredProperty(key, parse(propSchema.object())); + } else { + s.optionalProperty(key, parse(propSchema.object())); + } + }); + } + return s; + } + + private AbstractArraySchema parseArray(AbstractObject schema) { + if(!schema.string("type").equals("array")) + throw new IllegalArgumentException("Not a valid array schema"); + AbstractArraySchema s = new AbstractArraySchema(); + if(schema.has("items")) { + s.itemSchema(parse(schema.object("items"))); + } + if(schema.has("minItems")) { + s.min(schema.number("minItems").intValue()); + } + if(schema.has("maxItems")) { + s.max(schema.number("maxItems").intValue()); + } + return s; + } + +} From 52b747efab0acd33f7913cc833b1ccdf9305e26e Mon Sep 17 00:00:00 2001 From: maherp1 Date: Tue, 19 Dec 2023 19:44:50 +0100 Subject: [PATCH 07/19] Implemented JsonSchemaParser and several new validation rules --- .../schema/AbstractArraySchema.java | 53 ++++++- .../schema/AbstractBooleanSchema.java | 11 ++ .../schema/AbstractNumberSchema.java | 68 ++++++++- .../schema/AbstractObjectSchema.java | 25 ++++ .../abstractdata/schema/AbstractSchema.java | 1 + .../schema/AbstractStringSchema.java | 28 +++- .../abstractdata/schema/JsonSchemaParser.java | 129 ++++++++++++------ .../abstractdata/schema/OneOfSchema.java | 13 ++ .../schema/SchemaValidationError.java | 2 + 9 files changed, 275 insertions(+), 55 deletions(-) diff --git a/src/main/java/org/javawebstack/abstractdata/schema/AbstractArraySchema.java b/src/main/java/org/javawebstack/abstractdata/schema/AbstractArraySchema.java index 7b2b6a6..5549696 100644 --- a/src/main/java/org/javawebstack/abstractdata/schema/AbstractArraySchema.java +++ b/src/main/java/org/javawebstack/abstractdata/schema/AbstractArraySchema.java @@ -2,11 +2,10 @@ import org.javawebstack.abstractdata.AbstractArray; import org.javawebstack.abstractdata.AbstractElement; +import org.javawebstack.abstractdata.AbstractObject; import org.javawebstack.abstractdata.AbstractPath; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; +import java.util.*; public class AbstractArraySchema implements AbstractSchema { @@ -14,6 +13,8 @@ public class AbstractArraySchema implements AbstractSchema { private Integer min; private Integer max; private boolean allowNull = false; + + private boolean unique; private final List> customValidations = new ArrayList<>(); public AbstractArraySchema itemSchema(AbstractSchema schema) { @@ -35,6 +36,10 @@ public AbstractArraySchema allowNull() { this.allowNull = true; return this; } + public AbstractArraySchema unique(){ + this.unique = true; + return this; + } public AbstractArraySchema customValidation(CustomValidation validation) { customValidations.add(validation); @@ -57,6 +62,25 @@ public List> getCustomValidations() { return customValidations; } + @Override + public AbstractObject toJsonSchema() { + AbstractObject obj = new AbstractObject(); + obj.set("type","array"); + if(min != null){ + obj.set("minItems",min); + } + if(max != null){ + obj.set("maxItems",max); + } + if(itemSchema != null){ + obj.set("items",itemSchema.toJsonSchema()); + } + if(unique) { + obj.set("uniqueItems","true"); + } + return obj; + } + public List validate(AbstractPath path, AbstractElement value) { List errors = new ArrayList<>(); if(value.getType() != AbstractElement.Type.ARRAY) { @@ -70,10 +94,11 @@ public List validate(AbstractPath path, AbstractElement v if(max != null && array.size() > max) { errors.add(new SchemaValidationError(path, "too_many_items").meta("max", String.valueOf(max)).meta("actual", String.valueOf(array.size()))); } - if(itemSchema != null) { - for(int i=0; i seen = new ArrayList<>(); + for(int i=0; i validate(AbstractPath path, AbstractElement v } errors.addAll(itemSchema.validate(itemPath, array.get(i))); } + + if(unique){ + if(seen.contains(item)){ + int originalIndex = seen.indexOf(item); + AbstractPath originalPath = path.subPath(String.valueOf(originalIndex)); + errors.add(new SchemaValidationError(itemPath,"duplicate_array_value") + .meta("value",item.toJsonString()) + .meta("first",originalPath.toString())); + } + seen.add(item); + } + } + + for(CustomValidation validation : customValidations) { errors.addAll(validation.validate(path, array)); } diff --git a/src/main/java/org/javawebstack/abstractdata/schema/AbstractBooleanSchema.java b/src/main/java/org/javawebstack/abstractdata/schema/AbstractBooleanSchema.java index 958e1a9..48cf614 100644 --- a/src/main/java/org/javawebstack/abstractdata/schema/AbstractBooleanSchema.java +++ b/src/main/java/org/javawebstack/abstractdata/schema/AbstractBooleanSchema.java @@ -1,6 +1,7 @@ package org.javawebstack.abstractdata.schema; import org.javawebstack.abstractdata.AbstractElement; +import org.javawebstack.abstractdata.AbstractObject; import org.javawebstack.abstractdata.AbstractPath; import org.javawebstack.abstractdata.AbstractPrimitive; @@ -31,6 +32,16 @@ public List> getCustomValidations() { return customValidations; } + @Override + public AbstractObject toJsonSchema() { + AbstractObject obj = new AbstractObject() + .set("type","boolean"); + if(staticValue != null){ + obj.set("const",staticValue); + } + return obj; + } + public List validate(AbstractPath path, AbstractElement value) { List errors = new ArrayList<>(); if(value.getType() != AbstractElement.Type.BOOLEAN) { diff --git a/src/main/java/org/javawebstack/abstractdata/schema/AbstractNumberSchema.java b/src/main/java/org/javawebstack/abstractdata/schema/AbstractNumberSchema.java index 8561c6c..31fc0be 100644 --- a/src/main/java/org/javawebstack/abstractdata/schema/AbstractNumberSchema.java +++ b/src/main/java/org/javawebstack/abstractdata/schema/AbstractNumberSchema.java @@ -1,6 +1,7 @@ package org.javawebstack.abstractdata.schema; import org.javawebstack.abstractdata.AbstractElement; +import org.javawebstack.abstractdata.AbstractObject; import org.javawebstack.abstractdata.AbstractPath; import org.javawebstack.abstractdata.AbstractPrimitive; @@ -14,18 +15,36 @@ public class AbstractNumberSchema implements AbstractSchema { private boolean integerOnly = false; private Number min; private Number max; + private boolean minExclusive = false; + private boolean maxExclusive = false; + private Number step; private final List> customValidations = new ArrayList<>(); public AbstractNumberSchema min(Number min) { + return min(min,false); + } + + public AbstractNumberSchema max(Number max) { + return max(max,false); + } + + public AbstractNumberSchema min(Number min, boolean exclusive) { + this.minExclusive = exclusive; this.min = min; return this; } - public AbstractNumberSchema max(Number max) { + public AbstractNumberSchema max(Number max, boolean exclusive) { + this.maxExclusive = exclusive; this.max = max; return this; } + public AbstractNumberSchema step(Number step){ + this.step = step; + return this; + } + public AbstractNumberSchema integerOnly() { this.integerOnly = true; return this; @@ -52,6 +71,36 @@ public List> getCustomValidations() { return customValidations; } + @Override + public AbstractObject toJsonSchema() { + AbstractObject obj = new AbstractObject(); + + obj.set("type",integerOnly ? "integer" : "number"); + if(min != null && max != null && !minExclusive && !maxExclusive){ + BigDecimal dMin = (min instanceof Float || min instanceof Double) ? BigDecimal.valueOf(min.doubleValue()) : BigDecimal.valueOf(min.longValue()); + BigDecimal dMax = (max instanceof Float || max instanceof Double) ? BigDecimal.valueOf(max.doubleValue()) : BigDecimal.valueOf(max.longValue()); + + if(dMin.compareTo(dMax)==0){ + obj.set("const",min); + } + + } + if(!obj.has("const")) { + if (min != null) { + obj.set(minExclusive ? "exclusiveMinimum" : "minimum", min); + } + if (max != null) { + obj.set(maxExclusive ? "exclusiveMaximum" : "maximum", max); + } + if (step != null) { + obj.set("multipleOf", step); + } + } + + + return obj; + } + public List validate(AbstractPath path, AbstractElement value) { List errors = new ArrayList<>(); if(value.getType() != AbstractElement.Type.NUMBER) { @@ -66,16 +115,27 @@ public List validate(AbstractPath path, AbstractElement v } if(min != null) { BigDecimal dMin = (min instanceof Float || min instanceof Double) ? BigDecimal.valueOf(min.doubleValue()) : BigDecimal.valueOf(min.longValue()); - if(dN.compareTo(dMin) < 0) { + if(!(dN.compareTo(dMin) > (minExclusive ? 0 : -1))) { errors.add(new SchemaValidationError(path, "number_smaller_than_min").meta("min", dMin.toPlainString()).meta("actual", dN.toPlainString())); } } if(max != null) { - BigDecimal dMax = (max instanceof Float || min instanceof Double) ? BigDecimal.valueOf(max.doubleValue()) : BigDecimal.valueOf(max.longValue()); - if(dN.compareTo(dMax) > 0) { + BigDecimal dMax = (max instanceof Float || max instanceof Double) ? BigDecimal.valueOf(max.doubleValue()) : BigDecimal.valueOf(max.longValue()); + if(!(dN.compareTo(dMax) < (maxExclusive ? 0 : 1))) { errors.add(new SchemaValidationError(path, "number_larger_than_max").meta("max", dMax.toPlainString()).meta("actual", dN.toPlainString())); } } + if(step != null) { + if(min != null && minExclusive){ + throw new UnsupportedOperationException("Step is not supported together with an exclusive minimum"); + } + BigDecimal dMin = min == null ? BigDecimal.ZERO : (min instanceof Float || min instanceof Double) ? BigDecimal.valueOf(min.doubleValue()) : BigDecimal.valueOf(min.longValue()); + BigDecimal dStep = (step instanceof Float || step instanceof Double) ? BigDecimal.valueOf(step.doubleValue()) : BigDecimal.valueOf(step.longValue()); + + if(dN.subtract(dMin).remainder(dStep).compareTo(BigDecimal.ZERO) != 0) { + errors.add(new SchemaValidationError(path, "number_not_within_step").meta("step", dStep.toPlainString()).meta("actual", dN.toPlainString()).meta("start",dMin.toPlainString())); + } + } for(CustomValidation validation : customValidations) { errors.addAll(validation.validate(path, value.primitive())); } diff --git a/src/main/java/org/javawebstack/abstractdata/schema/AbstractObjectSchema.java b/src/main/java/org/javawebstack/abstractdata/schema/AbstractObjectSchema.java index f3c4eaf..a920b02 100644 --- a/src/main/java/org/javawebstack/abstractdata/schema/AbstractObjectSchema.java +++ b/src/main/java/org/javawebstack/abstractdata/schema/AbstractObjectSchema.java @@ -1,5 +1,6 @@ package org.javawebstack.abstractdata.schema; +import org.javawebstack.abstractdata.AbstractArray; import org.javawebstack.abstractdata.AbstractElement; import org.javawebstack.abstractdata.AbstractObject; import org.javawebstack.abstractdata.AbstractPath; @@ -41,6 +42,30 @@ public AbstractObjectSchema additionalProperties(AbstractSchema schema) { return this; } + @Override + public AbstractObject toJsonSchema() { + AbstractObject obj = new AbstractObject(); + obj.set("type","object"); + AbstractObject properties = new AbstractObject(); + this.properties.forEach((key, value)->{ + properties.set(key,value.toJsonSchema()); + }); + obj.set("properties",properties); + + if(!requiredProperties.isEmpty()){ + AbstractArray required = new AbstractArray(); + requiredProperties.forEach(required::add); + obj.set("required",required); + } + if(!allowAdditionalProperties){ + obj.set("additionalProperties",false); + }else if(additionalPropertySchema != null){ + obj.set("additionalProperties",additionalPropertySchema.toJsonSchema()); + } + + return obj; + } + public List validate(AbstractPath path, AbstractElement value) { List errors = new ArrayList<>(); if(value.getType() != AbstractElement.Type.OBJECT) { diff --git a/src/main/java/org/javawebstack/abstractdata/schema/AbstractSchema.java b/src/main/java/org/javawebstack/abstractdata/schema/AbstractSchema.java index fa4acd0..9385246 100644 --- a/src/main/java/org/javawebstack/abstractdata/schema/AbstractSchema.java +++ b/src/main/java/org/javawebstack/abstractdata/schema/AbstractSchema.java @@ -7,6 +7,7 @@ import java.util.List; public interface AbstractSchema { + AbstractObject toJsonSchema(); default List validate(AbstractElement value) { return validate(AbstractPath.ROOT, value); diff --git a/src/main/java/org/javawebstack/abstractdata/schema/AbstractStringSchema.java b/src/main/java/org/javawebstack/abstractdata/schema/AbstractStringSchema.java index 10f6f46..8eb7f6f 100644 --- a/src/main/java/org/javawebstack/abstractdata/schema/AbstractStringSchema.java +++ b/src/main/java/org/javawebstack/abstractdata/schema/AbstractStringSchema.java @@ -1,8 +1,6 @@ package org.javawebstack.abstractdata.schema; -import org.javawebstack.abstractdata.AbstractElement; -import org.javawebstack.abstractdata.AbstractPath; -import org.javawebstack.abstractdata.AbstractPrimitive; +import org.javawebstack.abstractdata.*; import java.util.*; import java.util.regex.Matcher; @@ -73,6 +71,30 @@ public List> getCustomValidations() { return customValidations; } + @Override + public AbstractObject toJsonSchema() { + AbstractObject obj = new AbstractObject(); + obj.set("type","string"); + if(minLength != null){ + obj.set("minLength",minLength); + } + if(maxLength != null){ + obj.set("maxLength",maxLength); + } + if(staticValue != null) { + obj.set("const",staticValue); + } + if(regex != null) { + obj.set("pattern",regex); + } + if(enumValues != null) { + AbstractArray arr = new AbstractArray(enumValues.toArray()); + obj.set("enum",arr); + } + + return obj; + } + public List validate(AbstractPath path, AbstractElement value) { List errors = new ArrayList<>(); if(value.getType() != AbstractElement.Type.STRING) { diff --git a/src/main/java/org/javawebstack/abstractdata/schema/JsonSchemaParser.java b/src/main/java/org/javawebstack/abstractdata/schema/JsonSchemaParser.java index 3af934e..0eba0cf 100644 --- a/src/main/java/org/javawebstack/abstractdata/schema/JsonSchemaParser.java +++ b/src/main/java/org/javawebstack/abstractdata/schema/JsonSchemaParser.java @@ -4,8 +4,7 @@ import org.javawebstack.abstractdata.AbstractElement; import org.javawebstack.abstractdata.AbstractObject; -import java.util.Collections; -import java.util.List; +import java.util.*; /* { @@ -39,41 +38,6 @@ public class JsonSchemaParser { public static void main(String[] args) { - String schemaStr = "{\n" + - " \"$id\": \"1224\",\n" + - " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n" + - " \"title\": \"Person\",\n" + - " \"type\": \"object\",\n" + - " \"properties\": {\n" + - " \"name\": {\n" + - " \"type\": \"string\"\n" + - " },\n" + - " \"emails\": {\n" + - " \"type\": \"array\"\n" + - " \"minItems\": 1\n" + - " \"items\": {\n" + - " \"type\": \"string\"\n" + - " }\n" + - " },\n" + - " \"age\": {\n" + - " \"type\": \"integer\",\n" + - " \"minimum\": 0,\n" + - " \"maximum\": 120\n" + - " }\n" + - " },\n" + - " \"required\": [\n" + - " \"name\",\n" + - " \"emails\"\n" + - " ]\n" + - "}"; - AbstractObject schemaObj = AbstractElement.fromJson(schemaStr).object(); - AbstractSchema schema = new JsonSchemaParser().parse(schemaObj); - List errors = schema.validate(new AbstractObject() - .set("name", "Maher") - .set("emails", new AbstractArray()) - .set("age", 123) - ); - System.out.println(); } @@ -89,8 +53,12 @@ public AbstractSchema parse(AbstractObject schema) { case "string": { return parseString(schema); } + case "number": case "integer": { - return parseInteger(schema); + return parseNumber(schema); + } + case "boolean": { + return parseBoolean(schema); } default: { throw new UnsupportedOperationException("Unknown type: " + schema.string("type")); @@ -100,28 +68,77 @@ public AbstractSchema parse(AbstractObject schema) { if(schema.has("$ref")) { throw new UnsupportedOperationException("$ref is currently not supported"); } + if(schema.has("oneOf")){ + return parseOneOf(schema); + } throw new IllegalArgumentException("Invalid json schema"); } + private OneOfSchema parseOneOf(AbstractObject schema) { + if(!schema.hasArray("oneOf")){ + throw new IllegalArgumentException("Not a valid oneOf schema"); + } + AbstractArray absArr = schema.array("oneOf"); + AbstractSchema[] schemas = new AbstractSchema[absArr.size()]; + for(int i = 0; i(schema.array("enum").toStringList())); + } + if(schema.has("const")){ + s.staticValue(schema.string("const")); + } return s; } - private AbstractNumberSchema parseInteger(AbstractObject schema) { - if(!schema.string("type").equals("integer")) - throw new IllegalArgumentException("Not a valid integer schema"); + private AbstractNumberSchema parseNumber(AbstractObject schema) { + boolean isInteger = schema.string("type").equals("integer"); + + if (!isInteger && !schema.string("type").equals("number")) { + throw new IllegalArgumentException("Not a valid number schema"); + } AbstractNumberSchema s = new AbstractNumberSchema(); + if(isInteger){ + s.integerOnly(); + } if(schema.has("minimum")) s.min(schema.number("minimum")); if(schema.has("maximum")) s.max(schema.number("maximum")); + if(schema.has("exclusiveMinimum")) + s.min(schema.number("exclusiveMinimum"),true); + if(schema.has("exclusiveMaximum")) + s.max(schema.number("exclusiveMaximum"),true); + if(schema.has("const")){ + Number staticValue = schema.number("const"); + s.min(staticValue); + s.max(staticValue); + } + if(schema.has("multipleOf")){ + s.step(schema.number("multipleOf")); + } + return s; } + private AbstractObjectSchema parseObject(AbstractObject schema) { if(!schema.string("type").equals("object")) throw new IllegalArgumentException("Not a valid object schema"); @@ -136,6 +153,20 @@ private AbstractObjectSchema parseObject(AbstractObject schema) { } }); } + AbstractElement additionalProperties = schema.get("additionalProperties"); + if(additionalProperties != null){ + if(additionalProperties.isBoolean()){ + if(additionalProperties.bool()){ + s.additionalProperties(); + } + } else { + s.additionalProperties(parse(additionalProperties.object())); + } + + + } else { + s.additionalProperties(); + } return s; } @@ -152,7 +183,23 @@ private AbstractArraySchema parseArray(AbstractObject schema) { if(schema.has("maxItems")) { s.max(schema.number("maxItems").intValue()); } + if(schema.has("uniqueItems") && schema.bool("uniqueItems")){ + s.unique(); + } return s; } + private AbstractBooleanSchema parseBoolean(AbstractObject schema){ + if(!schema.string("type").equals("boolean")) + throw new IllegalArgumentException("Not a valid boolean schema"); + AbstractBooleanSchema s = new AbstractBooleanSchema(); + if(schema.has("const")){ + s.staticValue(schema.bool("const")); + } + + + return new AbstractBooleanSchema(); + + } + } diff --git a/src/main/java/org/javawebstack/abstractdata/schema/OneOfSchema.java b/src/main/java/org/javawebstack/abstractdata/schema/OneOfSchema.java index 4960632..556b326 100644 --- a/src/main/java/org/javawebstack/abstractdata/schema/OneOfSchema.java +++ b/src/main/java/org/javawebstack/abstractdata/schema/OneOfSchema.java @@ -1,6 +1,8 @@ package org.javawebstack.abstractdata.schema; +import org.javawebstack.abstractdata.AbstractArray; import org.javawebstack.abstractdata.AbstractElement; +import org.javawebstack.abstractdata.AbstractObject; import org.javawebstack.abstractdata.AbstractPath; import java.util.ArrayList; @@ -17,6 +19,17 @@ public OneOfSchema(AbstractSchema... schemas) { this.schemas.addAll(Arrays.asList(schemas)); } + @Override + public AbstractObject toJsonSchema() { + AbstractArray arr = new AbstractArray(); + for (AbstractSchema schema : schemas) { + arr.add(schema.toJsonSchema()); + } + + + return new AbstractObject().set("oneOf",arr); + } + public List validate(AbstractPath path, AbstractElement value) { List> schemaErrors = new ArrayList<>(); for(AbstractSchema schema : schemas) { diff --git a/src/main/java/org/javawebstack/abstractdata/schema/SchemaValidationError.java b/src/main/java/org/javawebstack/abstractdata/schema/SchemaValidationError.java index f516f39..68f0dee 100644 --- a/src/main/java/org/javawebstack/abstractdata/schema/SchemaValidationError.java +++ b/src/main/java/org/javawebstack/abstractdata/schema/SchemaValidationError.java @@ -22,6 +22,8 @@ public class SchemaValidationError { put("value_too_short", "The length of the value ({actual}) is shorter than the minimum of {min}"); put("value_too_long", "The length of the value ({actual}) is longer than the maximum of {max}"); put("invalid_pattern", "The value '{actual}' does not match the pattern '{pattern}'"); + put("number_not_within_step", "The value '{start}' was not in steps of {step} starting from {start}"); + put("duplicate_array_value", "The value '{value}' is a duplicate of '{first}'"); }}; private AbstractPath path; From db7d4d31cef8297664b16caaa9ad9794a095f280 Mon Sep 17 00:00:00 2001 From: JanHolger Date: Tue, 19 Dec 2023 19:49:30 +0100 Subject: [PATCH 08/19] Optimized imports and reformatted code --- .../abstractdata/AbstractArray.java | 16 ++-- .../abstractdata/AbstractNull.java | 2 +- .../abstractdata/AbstractObject.java | 12 +-- .../abstractdata/AbstractPath.java | 22 +++--- .../abstractdata/AbstractPrimitive.java | 33 +++++---- .../abstractdata/NamingPolicy.java | 2 +- .../abstractdata/bson/BsonConverter.java | 53 +++++++------- .../abstractdata/bson/BsonTypeAdapter.java | 16 ++-- .../collector/AbstractArrayCollector.java | 5 +- .../collector/AbstractObjectCollector.java | 5 +- .../abstractdata/json/JsonDumper.java | 46 ++++++------ .../abstractdata/json/JsonParser.java | 52 ++++++------- .../abstractdata/mapper/Mapper.java | 28 +++---- .../abstractdata/mapper/MapperContext.java | 4 +- .../mapper/MapperTypeAdapter.java | 2 + .../abstractdata/mapper/MapperTypeSpec.java | 32 ++++---- .../mapper/annotation/DateFormat.java | 3 + .../mapper/annotation/MapperOptions.java | 6 ++ .../mapper/naming/CamelCaseNamingPolicy.java | 2 +- .../mapper/naming/NamingPolicy.java | 1 + .../schema/AbstractArraySchema.java | 51 +++++++------ .../schema/AbstractBooleanSchema.java | 12 +-- .../schema/AbstractNumberSchema.java | 40 +++++----- .../schema/AbstractObjectSchema.java | 38 +++++----- .../schema/AbstractStringSchema.java | 40 +++++----- .../abstractdata/schema/JsonSchemaParser.java | 73 ++++++++++--------- .../abstractdata/schema/OneOfSchema.java | 12 +-- .../schema/SchemaValidationError.java | 8 +- .../abstractdata/util/Helpers.java | 30 ++++---- .../abstractdata/xml/LegacyXMLParser.java | 4 +- .../abstractdata/xml/XMLDumper.java | 18 ++--- .../abstractdata/xml/XMLElement.java | 8 +- 32 files changed, 351 insertions(+), 325 deletions(-) diff --git a/src/main/java/org/javawebstack/abstractdata/AbstractArray.java b/src/main/java/org/javawebstack/abstractdata/AbstractArray.java index 2d97ea4..a94faaa 100644 --- a/src/main/java/org/javawebstack/abstractdata/AbstractArray.java +++ b/src/main/java/org/javawebstack/abstractdata/AbstractArray.java @@ -22,10 +22,10 @@ public AbstractArray array(boolean strict) throws AbstractCoercingException { } public AbstractObject object(boolean strict) throws AbstractCoercingException { - if(strict) + if (strict) throw new AbstractCoercingException(Type.OBJECT, Type.ARRAY); AbstractObject object = new AbstractObject(); - for(int i=0; i< elements.size(); i++) + for (int i = 0; i < elements.size(); i++) object.set(String.valueOf(i), elements.get(i)); return object; } @@ -227,11 +227,11 @@ public AbstractElement query(String query) { try { int index = Integer.parseInt(q[0]); AbstractElement e = get(index); - if(e == null || q.length == 1) + if (e == null || q.length == 1) return e; - if(e.isObject()) + if (e.isObject()) return e.object().query(q[1]); - if(e.isArray()) + if (e.isArray()) return e.array().query(q[1]); return null; } catch (NumberFormatException nfe) { @@ -281,7 +281,7 @@ public List toStringList() { public List toStringList(boolean strict) { List list = new ArrayList<>(); - for(AbstractElement e : elements) + for (AbstractElement e : elements) list.add(e.string(strict)); return list; } @@ -292,7 +292,7 @@ public List toObjectList() { public List toObjectList(boolean strict) { List list = new ArrayList<>(); - for(AbstractElement e : elements) + for (AbstractElement e : elements) list.add(e.object(strict)); return list; } @@ -365,7 +365,7 @@ public boolean equals(Object obj, boolean strict) { } } - public boolean equals (Object obj) { + public boolean equals(Object obj) { return equals(obj, false); } diff --git a/src/main/java/org/javawebstack/abstractdata/AbstractNull.java b/src/main/java/org/javawebstack/abstractdata/AbstractNull.java index de84b06..cb949b5 100644 --- a/src/main/java/org/javawebstack/abstractdata/AbstractNull.java +++ b/src/main/java/org/javawebstack/abstractdata/AbstractNull.java @@ -60,7 +60,7 @@ public boolean equals(Object obj, boolean strict) { return obj == null || obj instanceof AbstractNull; } - public boolean equals (Object obj) { + public boolean equals(Object obj) { return equals(obj, false); } } diff --git a/src/main/java/org/javawebstack/abstractdata/AbstractObject.java b/src/main/java/org/javawebstack/abstractdata/AbstractObject.java index 4130884..904f6d8 100644 --- a/src/main/java/org/javawebstack/abstractdata/AbstractObject.java +++ b/src/main/java/org/javawebstack/abstractdata/AbstractObject.java @@ -71,7 +71,7 @@ public AbstractObject object(boolean strict) throws AbstractCoercingException { } public AbstractArray array(boolean strict) throws AbstractCoercingException { - if(strict) + if (strict) throw new AbstractCoercingException(Type.ARRAY, Type.OBJECT); AbstractArray array = new AbstractArray(); for (int i = 0; i < size(); i++) { @@ -94,17 +94,17 @@ public AbstractElement get(String key, AbstractElement orElse) { public AbstractElement query(String query) { String[] q = query.split("\\.", 2); AbstractElement e = get(q[0]); - if(e == null || q.length == 1) + if (e == null || q.length == 1) return e; - if(e.isObject()) + if (e.isObject()) return e.object().query(q[1]); - if(e.isArray()) + if (e.isArray()) return e.array().query(q[1]); return null; } public AbstractElement query(String query, AbstractElement orElse) { - if(orElse == null) + if (orElse == null) orElse = AbstractNull.VALUE; AbstractElement value = query(query); return (value != null && !value.isNull()) ? value : orElse; @@ -288,7 +288,7 @@ public boolean equals(Object obj, boolean strict) { } } - public boolean equals (Object obj) { + public boolean equals(Object obj) { return equals(obj, false); } } diff --git a/src/main/java/org/javawebstack/abstractdata/AbstractPath.java b/src/main/java/org/javawebstack/abstractdata/AbstractPath.java index e64640e..0347f2e 100644 --- a/src/main/java/org/javawebstack/abstractdata/AbstractPath.java +++ b/src/main/java/org/javawebstack/abstractdata/AbstractPath.java @@ -13,7 +13,7 @@ public class AbstractPath { public AbstractPath(String name) { this(ROOT, name); - if(name == null || name.isEmpty()) + if (name == null || name.isEmpty()) throw new IllegalArgumentException("Name can not be null or empty"); } @@ -43,14 +43,14 @@ public AbstractPath clone() { public AbstractPath concat(AbstractPath path) { AbstractPath cloned = clone(); - for(String part : path.getParts()) + for (String part : path.getParts()) cloned = cloned.subPath(part); return cloned; } public List getParts() { List parts = parent != null ? parent.getParts() : new ArrayList<>(); - if(name != null) + if (name != null) parts.add(name); return parts; } @@ -61,13 +61,13 @@ public String toString() { public static AbstractPath parse(String s) { s = s.trim(); - if(s.isEmpty()) + if (s.isEmpty()) return ROOT; String[] spl = s.split("\\."); AbstractPath path = new AbstractPath(spl[0]); - for(int i=1; i type) throws MapperException { BsonValue value = converter.toBson(element); - if(value instanceof BsonNull && !BsonNull.class.equals(type)) + if (value instanceof BsonNull && !BsonNull.class.equals(type)) return null; - if(byte[].class.equals(type)) { + if (byte[].class.equals(type)) { BasicOutputBuffer outputBuffer = new BasicOutputBuffer(); new BsonDocumentCodec().encode(new BsonBinaryWriter(outputBuffer), value.asDocument(), EncoderContext.builder().build()); return outputBuffer.toByteArray(); } - if(ObjectId.class.equals(type)) + if (ObjectId.class.equals(type)) return value.asObjectId().getValue(); - if(Decimal128.class.equals(type)) + if (Decimal128.class.equals(type)) return value.asDecimal128().getValue(); return value; } public Class[] getSupportedTypes() { - return new Class[] { + return new Class[]{ ObjectId.class, Decimal128.class, BsonValue.class, diff --git a/src/main/java/org/javawebstack/abstractdata/collector/AbstractArrayCollector.java b/src/main/java/org/javawebstack/abstractdata/collector/AbstractArrayCollector.java index d924278..f9cd104 100644 --- a/src/main/java/org/javawebstack/abstractdata/collector/AbstractArrayCollector.java +++ b/src/main/java/org/javawebstack/abstractdata/collector/AbstractArrayCollector.java @@ -30,7 +30,10 @@ public BiConsumer, T> accumulator() { } public BinaryOperator> combiner() { - return (l1, l2) -> { l1.addAll(l2); return l1; }; + return (l1, l2) -> { + l1.addAll(l2); + return l1; + }; } public Function, AbstractArray> finisher() { diff --git a/src/main/java/org/javawebstack/abstractdata/collector/AbstractObjectCollector.java b/src/main/java/org/javawebstack/abstractdata/collector/AbstractObjectCollector.java index 8b53da0..2cefabc 100644 --- a/src/main/java/org/javawebstack/abstractdata/collector/AbstractObjectCollector.java +++ b/src/main/java/org/javawebstack/abstractdata/collector/AbstractObjectCollector.java @@ -32,7 +32,10 @@ public BiConsumer, T> accumulator() { } public BinaryOperator> combiner() { - return (m1, m2) -> { m1.putAll(m2); return m1; }; + return (m1, m2) -> { + m1.putAll(m2); + return m1; + }; } public Function, AbstractObject> finisher() { diff --git a/src/main/java/org/javawebstack/abstractdata/json/JsonDumper.java b/src/main/java/org/javawebstack/abstractdata/json/JsonDumper.java index 375e639..a8097f3 100644 --- a/src/main/java/org/javawebstack/abstractdata/json/JsonDumper.java +++ b/src/main/java/org/javawebstack/abstractdata/json/JsonDumper.java @@ -29,31 +29,31 @@ public String dump(AbstractElement element) { } private List dumpLines(AbstractElement element) { - if(element == null || element.isNull()) + if (element == null || element.isNull()) return Collections.singletonList("null"); - if(element.isBoolean()) + if (element.isBoolean()) return Collections.singletonList(element.bool().toString()); - if(element.isNumber()) + if (element.isNumber()) return Collections.singletonList(element.number().toString()); - if(element.isString()) + if (element.isString()) return Collections.singletonList("\"" + escape(element.string()) + "\""); - if(element.isObject()) { - List lines =new ArrayList<>(); - if(element.object().size() == 0) { + if (element.isObject()) { + List lines = new ArrayList<>(); + if (element.object().size() == 0) { lines.add("{}"); return lines; } - if(pretty) { + if (pretty) { lines.add("{"); List keys = new ArrayList<>(element.object().keys()); - for(int i=0; i vLines = dumpLines(element.object().get(k)); lines.add(indent + "\"" + escape(k) + "\": " + vLines.get(0)); - for(int j=1; j dumpLines(AbstractElement element) { } return lines; } - if(element.isArray()) { - List lines =new ArrayList<>(); - if(element.array().size() == 0) { + if (element.isArray()) { + List lines = new ArrayList<>(); + if (element.array().size() == 0) { lines.add("[]"); return lines; } - if(pretty) { + if (pretty) { lines.add("["); AbstractArray array = element.array(); - for(int i=0; i vLines = dumpLines(array.get(i)); - for(int j=0; j dumpLines(AbstractElement element) { private static String escape(String s) { StringBuilder sb = new StringBuilder(); - for(int i=0; i= '\u007F' && ch <= '\u009F' || ch >= '\u2000' && ch <= '\u20FF') { String hex = Integer.toHexString(ch); sb.append("\\u"); - for(int k=0; k < 4-hex.length(); k++) + for (int k = 0; k < 4 - hex.length(); k++) sb.append('0'); sb.append(hex.toUpperCase(Locale.ROOT)); } else { diff --git a/src/main/java/org/javawebstack/abstractdata/json/JsonParser.java b/src/main/java/org/javawebstack/abstractdata/json/JsonParser.java index 549f096..1afedd2 100644 --- a/src/main/java/org/javawebstack/abstractdata/json/JsonParser.java +++ b/src/main/java/org/javawebstack/abstractdata/json/JsonParser.java @@ -13,7 +13,7 @@ public class JsonParser { public AbstractElement parse(String json) throws ParseException { char[] primChars = json.toCharArray(); List chars = new ArrayList<>(primChars.length); - for(int i=0; i stack = new ArrayDeque<>(chars); AbstractElement parsed; @@ -22,11 +22,11 @@ public AbstractElement parse(String json) throws ParseException { } catch (NullPointerException ex) { throw new ParseException("Unexpected character ", primChars.length); } - if(parsed == null) { + if (parsed == null) { int line = 1; int pos = 1; - for(int i=0; i stack) { return parseString(stack); case 't': { stack.pop(); - if(stack.peek() != 'r') + if (stack.peek() != 'r') return null; stack.pop(); - if(stack.peek() != 'u') + if (stack.peek() != 'u') return null; stack.pop(); - if(stack.peek() != 'e') + if (stack.peek() != 'e') return null; stack.pop(); return new AbstractPrimitive(true); } case 'f': { stack.pop(); - if(stack.peek() != 'a') + if (stack.peek() != 'a') return null; stack.pop(); - if(stack.peek() != 'l') + if (stack.peek() != 'l') return null; stack.pop(); - if(stack.peek() != 's') + if (stack.peek() != 's') return null; stack.pop(); - if(stack.peek() != 'e') + if (stack.peek() != 'e') return null; stack.pop(); return new AbstractPrimitive(false); } case 'n': { stack.pop(); - if(stack.peek() != 'u') + if (stack.peek() != 'u') return null; stack.pop(); - if(stack.peek() != 'l') + if (stack.peek() != 'l') return null; stack.pop(); - if(stack.peek() != 'l') + if (stack.peek() != 'l') return null; stack.pop(); return AbstractNull.VALUE; @@ -117,11 +117,11 @@ private AbstractPrimitive parseNumber(Deque stack) { while (Character.isDigit(stack.peek()) || stack.peek() == '.' || stack.peek() == '-' || stack.peek() == 'E' || stack.peek() == 'e') sb.append(stack.pop()); String s = sb.toString(); - if(s.contains(".")) { + if (s.contains(".")) { return new AbstractPrimitive(Double.parseDouble(s)); } else { long l = Long.parseLong(s); - if(l >= Integer.MIN_VALUE && l <= Integer.MAX_VALUE) + if (l >= Integer.MIN_VALUE && l <= Integer.MAX_VALUE) return new AbstractPrimitive((int) l); return new AbstractPrimitive(l); } @@ -132,9 +132,9 @@ private AbstractPrimitive parseString(Deque stack) { StringBuilder sb = new StringBuilder(); while (true) { char c = stack.pop(); - if(c == '"') + if (c == '"') break; - if(c == '\\') { + if (c == '\\') { c = stack.pop(); switch (c) { case 'n': @@ -176,24 +176,24 @@ private AbstractObject parseObject(Deque stack) { AbstractObject object = new AbstractObject(); while (true) { popWhitespace(stack); - if(stack.peek() == '}') { + if (stack.peek() == '}') { stack.pop(); break; } AbstractPrimitive key = parseString(stack); - if(key == null) + if (key == null) return null; popWhitespace(stack); - if(stack.peek() != ':') + if (stack.peek() != ':') return null; stack.pop(); popWhitespace(stack); AbstractElement value = parse(stack); - if(value == null) + if (value == null) return null; object.set(key.string(), value); popWhitespace(stack); - if(stack.peek() == ',') + if (stack.peek() == ',') stack.pop(); } return object; @@ -204,16 +204,16 @@ private AbstractArray parseArray(Deque stack) { AbstractArray array = new AbstractArray(); while (true) { popWhitespace(stack); - if(stack.peek() == ']') { + if (stack.peek() == ']') { stack.pop(); break; } AbstractElement value = parse(stack); - if(value == null) + if (value == null) return null; array.add(value); popWhitespace(stack); - if(stack.peek() == ',') + if (stack.peek() == ',') stack.pop(); } return array; diff --git a/src/main/java/org/javawebstack/abstractdata/mapper/Mapper.java b/src/main/java/org/javawebstack/abstractdata/mapper/Mapper.java index ad2f235..64a4b4f 100644 --- a/src/main/java/org/javawebstack/abstractdata/mapper/Mapper.java +++ b/src/main/java/org/javawebstack/abstractdata/mapper/Mapper.java @@ -29,27 +29,27 @@ public T map(AbstractElement element, Class type) throws MapperException } public T map(MapperContext context, AbstractElement element, Class type) throws MapperException { - if(type == null || element == null || element.isNull()) + if (type == null || element == null || element.isNull()) return null; - if(type.isArray()) { - if(!element.isArray()) + if (type.isArray()) { + if (!element.isArray()) throw new MapperWrongTypeException(null, "array", Helpers.typeName(element)); Object arr = Array.newInstance(type.getComponentType(), element.array().size()); - for(int i=0; i type) { MapperTypeAdapter adapter = adapters.get(type); - if(adapter != null) + if (adapter != null) return adapter; - for(Class t : adapters.keySet()) { - if(t.isAssignableFrom(type)) { + for (Class t : adapters.keySet()) { + if (t.isAssignableFrom(type)) { return adapters.get(t); } } @@ -61,15 +61,15 @@ public AbstractElement map(Object obj) throws MapperException { } public AbstractElement map(MapperContext context, Object obj) throws MapperException { - if(obj == null) + if (obj == null) return AbstractNull.VALUE; - if(obj.getClass().isArray()) { + if (obj.getClass().isArray()) { AbstractArray array = new AbstractArray(); - for(int i=0; i type, MapperTypeAdapter adapter) { public Mapper adapter(MapperTypeAdapter adapter) { Class[] types = adapter.getSupportedTypes(); - if(types != null) { - for(Class type : types) + if (types != null) { + for (Class type : types) adapter(type, adapter); } return this; diff --git a/src/main/java/org/javawebstack/abstractdata/mapper/MapperContext.java b/src/main/java/org/javawebstack/abstractdata/mapper/MapperContext.java index baecb73..1b90aac 100644 --- a/src/main/java/org/javawebstack/abstractdata/mapper/MapperContext.java +++ b/src/main/java/org/javawebstack/abstractdata/mapper/MapperContext.java @@ -47,13 +47,13 @@ public Map, List> getAnnotations() { } public List getAnnotations(Class type) { - if(!annotations.containsKey(type)) + if (!annotations.containsKey(type)) return new ArrayList<>(); return (List) annotations.get(type); } public T getAnnotation(Class type) { - if(!annotations.containsKey(type)) + if (!annotations.containsKey(type)) return null; return (T) annotations.get(type).get(0); } diff --git a/src/main/java/org/javawebstack/abstractdata/mapper/MapperTypeAdapter.java b/src/main/java/org/javawebstack/abstractdata/mapper/MapperTypeAdapter.java index 7c3fb0a..449f70e 100644 --- a/src/main/java/org/javawebstack/abstractdata/mapper/MapperTypeAdapter.java +++ b/src/main/java/org/javawebstack/abstractdata/mapper/MapperTypeAdapter.java @@ -6,7 +6,9 @@ public interface MapperTypeAdapter { AbstractElement toAbstract(MapperContext context, Object value) throws MapperException; + Object fromAbstract(MapperContext context, AbstractElement element, Class type) throws MapperException; + default Class[] getSupportedTypes() { return null; } diff --git a/src/main/java/org/javawebstack/abstractdata/mapper/MapperTypeSpec.java b/src/main/java/org/javawebstack/abstractdata/mapper/MapperTypeSpec.java index d8c01d1..de0b2d3 100644 --- a/src/main/java/org/javawebstack/abstractdata/mapper/MapperTypeSpec.java +++ b/src/main/java/org/javawebstack/abstractdata/mapper/MapperTypeSpec.java @@ -15,19 +15,19 @@ public class MapperTypeSpec { private static final Map, MapperTypeSpec> typeSpecs = new HashMap<>(); public static MapperTypeSpec get(Class type) throws MapperException { - if(type.isArray() || type.isEnum() || type.isPrimitive()) + if (type.isArray() || type.isEnum() || type.isPrimitive()) return null; - if(Integer.class.equals(type)) + if (Integer.class.equals(type)) return null; - if(Long.class.equals(type)) + if (Long.class.equals(type)) return null; - if(Double.class.equals(type)) + if (Double.class.equals(type)) return null; - if(Float.class.equals(type)) + if (Float.class.equals(type)) return null; - if(Short.class.equals(type)) + if (Short.class.equals(type)) return null; - if(Boolean.class.equals(type)) + if (Boolean.class.equals(type)) return null; return typeSpecs.computeIfAbsent(type, MapperTypeSpec::new); } @@ -49,8 +49,8 @@ public MapperTypeSpec(Class type) throws MapperException { current = current.getSuperclass(); } while (!Object.class.equals(current)); // Collect parent classes recursively while (!classes.empty()) { - for(Field f : classes.pop().getDeclaredFields()) { - if(Modifier.isStatic(f.getModifiers())) // Don't include static fields + for (Field f : classes.pop().getDeclaredFields()) { + if (Modifier.isStatic(f.getModifiers())) // Don't include static fields continue; checkoutField(f); } @@ -60,11 +60,11 @@ public MapperTypeSpec(Class type) throws MapperException { private void checkoutField(Field field) throws MapperException { Map, List> annotations = new HashMap<>(); - for(Annotation annotation : field.getDeclaredAnnotations()) + for (Annotation annotation : field.getDeclaredAnnotations()) annotations.computeIfAbsent(annotation.annotationType(), k -> new ArrayList<>()).add(annotation); - if(annotations.containsKey(Additional.class)) { - if(!field.getType().equals(AbstractObject.class)) + if (annotations.containsKey(Additional.class)) { + if (!field.getType().equals(AbstractObject.class)) throw new MapperException("Additional field '" + field.getName() + "' in type '" + field.getDeclaringClass().getName() + "' needs to be of type AbstractObject, found '" + field.getType().getName() + "'"); additionalField = field; return; @@ -76,11 +76,11 @@ private void checkoutField(Field field) throws MapperException { spec.field = field; spec.annotations = annotations; - if(annotations.containsKey(MapperOptions.class)) { + if (annotations.containsKey(MapperOptions.class)) { MapperOptions options = (MapperOptions) annotations.get(MapperOptions.class).get(0); - if(options.name().length() > 0) + if (options.name().length() > 0) spec.name = options.name(); - if(!options.adapter().equals(MapperTypeAdapter.class)) { + if (!options.adapter().equals(MapperTypeAdapter.class)) { try { spec.adapter = options.adapter().newInstance(); } catch (InstantiationException | IllegalAccessException e) { @@ -104,7 +104,7 @@ public List getFieldSpecs() { } public Field getAdditionalField() { - if(additionalField != null) + if (additionalField != null) additionalField.setAccessible(true); return additionalField; } diff --git a/src/main/java/org/javawebstack/abstractdata/mapper/annotation/DateFormat.java b/src/main/java/org/javawebstack/abstractdata/mapper/annotation/DateFormat.java index d752599..cc74d26 100644 --- a/src/main/java/org/javawebstack/abstractdata/mapper/annotation/DateFormat.java +++ b/src/main/java/org/javawebstack/abstractdata/mapper/annotation/DateFormat.java @@ -10,8 +10,11 @@ public @interface DateFormat { String value() default ""; + String timezone() default ""; + boolean epoch() default false; + boolean millis() default false; } diff --git a/src/main/java/org/javawebstack/abstractdata/mapper/annotation/MapperOptions.java b/src/main/java/org/javawebstack/abstractdata/mapper/annotation/MapperOptions.java index 6a8947d..5900e4e 100644 --- a/src/main/java/org/javawebstack/abstractdata/mapper/annotation/MapperOptions.java +++ b/src/main/java/org/javawebstack/abstractdata/mapper/annotation/MapperOptions.java @@ -12,11 +12,17 @@ public @interface MapperOptions { String name() default ""; + int order() default 0; + Class[] generic() default {}; + boolean expose() default false; + boolean hidden() default false; + boolean omitNull() default true; + Class adapter() default MapperTypeAdapter.class; } diff --git a/src/main/java/org/javawebstack/abstractdata/mapper/naming/CamelCaseNamingPolicy.java b/src/main/java/org/javawebstack/abstractdata/mapper/naming/CamelCaseNamingPolicy.java index 9b7620c..c22a80b 100644 --- a/src/main/java/org/javawebstack/abstractdata/mapper/naming/CamelCaseNamingPolicy.java +++ b/src/main/java/org/javawebstack/abstractdata/mapper/naming/CamelCaseNamingPolicy.java @@ -9,7 +9,7 @@ public class CamelCaseNamingPolicy implements NamingPolicy { public String toAbstract(String source) { List words = Helpers.words(source); StringBuilder sb = new StringBuilder(words.get(0)); - for(int i=1; i fieldNames); } diff --git a/src/main/java/org/javawebstack/abstractdata/schema/AbstractArraySchema.java b/src/main/java/org/javawebstack/abstractdata/schema/AbstractArraySchema.java index 5549696..dd14a37 100644 --- a/src/main/java/org/javawebstack/abstractdata/schema/AbstractArraySchema.java +++ b/src/main/java/org/javawebstack/abstractdata/schema/AbstractArraySchema.java @@ -5,7 +5,9 @@ import org.javawebstack.abstractdata.AbstractObject; import org.javawebstack.abstractdata.AbstractPath; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; public class AbstractArraySchema implements AbstractSchema { @@ -36,7 +38,8 @@ public AbstractArraySchema allowNull() { this.allowNull = true; return this; } - public AbstractArraySchema unique(){ + + public AbstractArraySchema unique() { this.unique = true; return this; } @@ -65,42 +68,42 @@ public List> getCustomValidations() { @Override public AbstractObject toJsonSchema() { AbstractObject obj = new AbstractObject(); - obj.set("type","array"); - if(min != null){ - obj.set("minItems",min); + obj.set("type", "array"); + if (min != null) { + obj.set("minItems", min); } - if(max != null){ - obj.set("maxItems",max); + if (max != null) { + obj.set("maxItems", max); } - if(itemSchema != null){ - obj.set("items",itemSchema.toJsonSchema()); + if (itemSchema != null) { + obj.set("items", itemSchema.toJsonSchema()); } - if(unique) { - obj.set("uniqueItems","true"); + if (unique) { + obj.set("uniqueItems", "true"); } return obj; } public List validate(AbstractPath path, AbstractElement value) { List errors = new ArrayList<>(); - if(value.getType() != AbstractElement.Type.ARRAY) { + if (value.getType() != AbstractElement.Type.ARRAY) { errors.add(new SchemaValidationError(path, "invalid_type").meta("expected", "array").meta("actual", value.getType().name().toLowerCase(Locale.ROOT))); return errors; } AbstractArray array = value.array(); - if(min != null && array.size() < min) { + if (min != null && array.size() < min) { errors.add(new SchemaValidationError(path, "not_enough_items").meta("min", String.valueOf(min)).meta("actual", String.valueOf(array.size()))); } - if(max != null && array.size() > max) { + if (max != null && array.size() > max) { errors.add(new SchemaValidationError(path, "too_many_items").meta("max", String.valueOf(max)).meta("actual", String.valueOf(array.size()))); } List seen = new ArrayList<>(); - for(int i=0; i validate(AbstractPath path, AbstractElement v errors.addAll(itemSchema.validate(itemPath, array.get(i))); } - if(unique){ - if(seen.contains(item)){ + if (unique) { + if (seen.contains(item)) { int originalIndex = seen.indexOf(item); AbstractPath originalPath = path.subPath(String.valueOf(originalIndex)); - errors.add(new SchemaValidationError(itemPath,"duplicate_array_value") - .meta("value",item.toJsonString()) - .meta("first",originalPath.toString())); + errors.add(new SchemaValidationError(itemPath, "duplicate_array_value") + .meta("value", item.toJsonString()) + .meta("first", originalPath.toString())); } seen.add(item); } @@ -122,7 +125,7 @@ public List validate(AbstractPath path, AbstractElement v } - for(CustomValidation validation : customValidations) { + for (CustomValidation validation : customValidations) { errors.addAll(validation.validate(path, array)); } return errors; diff --git a/src/main/java/org/javawebstack/abstractdata/schema/AbstractBooleanSchema.java b/src/main/java/org/javawebstack/abstractdata/schema/AbstractBooleanSchema.java index 48cf614..8df83d5 100644 --- a/src/main/java/org/javawebstack/abstractdata/schema/AbstractBooleanSchema.java +++ b/src/main/java/org/javawebstack/abstractdata/schema/AbstractBooleanSchema.java @@ -35,23 +35,23 @@ public List> getCustomValidations() { @Override public AbstractObject toJsonSchema() { AbstractObject obj = new AbstractObject() - .set("type","boolean"); - if(staticValue != null){ - obj.set("const",staticValue); + .set("type", "boolean"); + if (staticValue != null) { + obj.set("const", staticValue); } return obj; } public List validate(AbstractPath path, AbstractElement value) { List errors = new ArrayList<>(); - if(value.getType() != AbstractElement.Type.BOOLEAN) { + if (value.getType() != AbstractElement.Type.BOOLEAN) { errors.add(new SchemaValidationError(path, "invalid_type").meta("expected", "boolean").meta("actual", value.getType().name().toLowerCase(Locale.ROOT))); return errors; } - if(staticValue != null && staticValue != value.bool()) { + if (staticValue != null && staticValue != value.bool()) { errors.add(new SchemaValidationError(path, "invalid_static_value").meta("expected", staticValue.toString()).meta("actual", value.bool().toString())); } - for(CustomValidation validation : customValidations) { + for (CustomValidation validation : customValidations) { errors.addAll(validation.validate(path, value.primitive())); } return errors; diff --git a/src/main/java/org/javawebstack/abstractdata/schema/AbstractNumberSchema.java b/src/main/java/org/javawebstack/abstractdata/schema/AbstractNumberSchema.java index 31fc0be..0354bde 100644 --- a/src/main/java/org/javawebstack/abstractdata/schema/AbstractNumberSchema.java +++ b/src/main/java/org/javawebstack/abstractdata/schema/AbstractNumberSchema.java @@ -21,11 +21,11 @@ public class AbstractNumberSchema implements AbstractSchema { private final List> customValidations = new ArrayList<>(); public AbstractNumberSchema min(Number min) { - return min(min,false); + return min(min, false); } public AbstractNumberSchema max(Number max) { - return max(max,false); + return max(max, false); } public AbstractNumberSchema min(Number min, boolean exclusive) { @@ -40,7 +40,7 @@ public AbstractNumberSchema max(Number max, boolean exclusive) { return this; } - public AbstractNumberSchema step(Number step){ + public AbstractNumberSchema step(Number step) { this.step = step; return this; } @@ -75,17 +75,17 @@ public List> getCustomValidations() { public AbstractObject toJsonSchema() { AbstractObject obj = new AbstractObject(); - obj.set("type",integerOnly ? "integer" : "number"); - if(min != null && max != null && !minExclusive && !maxExclusive){ + obj.set("type", integerOnly ? "integer" : "number"); + if (min != null && max != null && !minExclusive && !maxExclusive) { BigDecimal dMin = (min instanceof Float || min instanceof Double) ? BigDecimal.valueOf(min.doubleValue()) : BigDecimal.valueOf(min.longValue()); BigDecimal dMax = (max instanceof Float || max instanceof Double) ? BigDecimal.valueOf(max.doubleValue()) : BigDecimal.valueOf(max.longValue()); - if(dMin.compareTo(dMax)==0){ - obj.set("const",min); + if (dMin.compareTo(dMax) == 0) { + obj.set("const", min); } } - if(!obj.has("const")) { + if (!obj.has("const")) { if (min != null) { obj.set(minExclusive ? "exclusiveMinimum" : "minimum", min); } @@ -103,40 +103,40 @@ public AbstractObject toJsonSchema() { public List validate(AbstractPath path, AbstractElement value) { List errors = new ArrayList<>(); - if(value.getType() != AbstractElement.Type.NUMBER) { + if (value.getType() != AbstractElement.Type.NUMBER) { errors.add(new SchemaValidationError(path, "invalid_type").meta("expected", integerOnly ? "integer" : "number").meta("actual", value.getType().name().toLowerCase(Locale.ROOT))); return errors; } Number n = value.number(); BigDecimal dN = (n instanceof Float || n instanceof Double) ? BigDecimal.valueOf(n.doubleValue()) : BigDecimal.valueOf(n.longValue()); - if(integerOnly && (n instanceof Float || n instanceof Double)) { + if (integerOnly && (n instanceof Float || n instanceof Double)) { errors.add(new SchemaValidationError(path, "invalid_type").meta("expected", "integer").meta("actual", "number")); return errors; } - if(min != null) { + if (min != null) { BigDecimal dMin = (min instanceof Float || min instanceof Double) ? BigDecimal.valueOf(min.doubleValue()) : BigDecimal.valueOf(min.longValue()); - if(!(dN.compareTo(dMin) > (minExclusive ? 0 : -1))) { + if (!(dN.compareTo(dMin) > (minExclusive ? 0 : -1))) { errors.add(new SchemaValidationError(path, "number_smaller_than_min").meta("min", dMin.toPlainString()).meta("actual", dN.toPlainString())); } } - if(max != null) { + if (max != null) { BigDecimal dMax = (max instanceof Float || max instanceof Double) ? BigDecimal.valueOf(max.doubleValue()) : BigDecimal.valueOf(max.longValue()); - if(!(dN.compareTo(dMax) < (maxExclusive ? 0 : 1))) { + if (!(dN.compareTo(dMax) < (maxExclusive ? 0 : 1))) { errors.add(new SchemaValidationError(path, "number_larger_than_max").meta("max", dMax.toPlainString()).meta("actual", dN.toPlainString())); } } - if(step != null) { - if(min != null && minExclusive){ + if (step != null) { + if (min != null && minExclusive) { throw new UnsupportedOperationException("Step is not supported together with an exclusive minimum"); } BigDecimal dMin = min == null ? BigDecimal.ZERO : (min instanceof Float || min instanceof Double) ? BigDecimal.valueOf(min.doubleValue()) : BigDecimal.valueOf(min.longValue()); - BigDecimal dStep = (step instanceof Float || step instanceof Double) ? BigDecimal.valueOf(step.doubleValue()) : BigDecimal.valueOf(step.longValue()); + BigDecimal dStep = (step instanceof Float || step instanceof Double) ? BigDecimal.valueOf(step.doubleValue()) : BigDecimal.valueOf(step.longValue()); - if(dN.subtract(dMin).remainder(dStep).compareTo(BigDecimal.ZERO) != 0) { - errors.add(new SchemaValidationError(path, "number_not_within_step").meta("step", dStep.toPlainString()).meta("actual", dN.toPlainString()).meta("start",dMin.toPlainString())); + if (dN.subtract(dMin).remainder(dStep).compareTo(BigDecimal.ZERO) != 0) { + errors.add(new SchemaValidationError(path, "number_not_within_step").meta("step", dStep.toPlainString()).meta("actual", dN.toPlainString()).meta("start", dMin.toPlainString())); } } - for(CustomValidation validation : customValidations) { + for (CustomValidation validation : customValidations) { errors.addAll(validation.validate(path, value.primitive())); } return errors; diff --git a/src/main/java/org/javawebstack/abstractdata/schema/AbstractObjectSchema.java b/src/main/java/org/javawebstack/abstractdata/schema/AbstractObjectSchema.java index a920b02..1b822b4 100644 --- a/src/main/java/org/javawebstack/abstractdata/schema/AbstractObjectSchema.java +++ b/src/main/java/org/javawebstack/abstractdata/schema/AbstractObjectSchema.java @@ -45,22 +45,22 @@ public AbstractObjectSchema additionalProperties(AbstractSchema schema) { @Override public AbstractObject toJsonSchema() { AbstractObject obj = new AbstractObject(); - obj.set("type","object"); + obj.set("type", "object"); AbstractObject properties = new AbstractObject(); - this.properties.forEach((key, value)->{ - properties.set(key,value.toJsonSchema()); + this.properties.forEach((key, value) -> { + properties.set(key, value.toJsonSchema()); }); - obj.set("properties",properties); + obj.set("properties", properties); - if(!requiredProperties.isEmpty()){ + if (!requiredProperties.isEmpty()) { AbstractArray required = new AbstractArray(); requiredProperties.forEach(required::add); - obj.set("required",required); + obj.set("required", required); } - if(!allowAdditionalProperties){ - obj.set("additionalProperties",false); - }else if(additionalPropertySchema != null){ - obj.set("additionalProperties",additionalPropertySchema.toJsonSchema()); + if (!allowAdditionalProperties) { + obj.set("additionalProperties", false); + } else if (additionalPropertySchema != null) { + obj.set("additionalProperties", additionalPropertySchema.toJsonSchema()); } return obj; @@ -68,26 +68,26 @@ public AbstractObject toJsonSchema() { public List validate(AbstractPath path, AbstractElement value) { List errors = new ArrayList<>(); - if(value.getType() != AbstractElement.Type.OBJECT) { + if (value.getType() != AbstractElement.Type.OBJECT) { errors.add(new SchemaValidationError(path, "invalid_type").meta("expected", "object").meta("actual", value.getType().name().toLowerCase(Locale.ROOT))); return errors; } AbstractObject object = value.object(); - for(String prop : requiredProperties) { - if(!object.has(prop) || object.get(prop).isNull()) { + for (String prop : requiredProperties) { + if (!object.has(prop) || object.get(prop).isNull()) { errors.add(new SchemaValidationError(path.subPath(prop), "missing_required_property")); } } - for(String prop : object.keys()) { + for (String prop : object.keys()) { AbstractElement propValue = object.get(prop); AbstractPath propPath = path.subPath(prop); - if(properties.containsKey(prop)) { - if(propValue.isNull()) + if (properties.containsKey(prop)) { + if (propValue.isNull()) continue; errors.addAll(properties.get(prop).validate(propPath, propValue)); } else { - if(allowAdditionalProperties) { - if(additionalPropertySchema != null) { + if (allowAdditionalProperties) { + if (additionalPropertySchema != null) { errors.addAll(additionalPropertySchema.validate(propPath, propValue)); } } else { @@ -95,7 +95,7 @@ public List validate(AbstractPath path, AbstractElement v } } } - for(CustomValidation validation : customValidations) { + for (CustomValidation validation : customValidations) { errors.addAll(validation.validate(path, object)); } return errors; diff --git a/src/main/java/org/javawebstack/abstractdata/schema/AbstractStringSchema.java b/src/main/java/org/javawebstack/abstractdata/schema/AbstractStringSchema.java index 8eb7f6f..bd1742f 100644 --- a/src/main/java/org/javawebstack/abstractdata/schema/AbstractStringSchema.java +++ b/src/main/java/org/javawebstack/abstractdata/schema/AbstractStringSchema.java @@ -23,7 +23,7 @@ public AbstractStringSchema staticValue(String value) { public AbstractStringSchema enumValues(Class> enumType) { Set values = new HashSet<>(); - for(Enum v : enumType.getEnumConstants()) { + for (Enum v : enumType.getEnumConstants()) { values.add(v.name()); } return enumValues(values); @@ -74,22 +74,22 @@ public List> getCustomValidations() { @Override public AbstractObject toJsonSchema() { AbstractObject obj = new AbstractObject(); - obj.set("type","string"); - if(minLength != null){ - obj.set("minLength",minLength); + obj.set("type", "string"); + if (minLength != null) { + obj.set("minLength", minLength); } - if(maxLength != null){ - obj.set("maxLength",maxLength); + if (maxLength != null) { + obj.set("maxLength", maxLength); } - if(staticValue != null) { - obj.set("const",staticValue); + if (staticValue != null) { + obj.set("const", staticValue); } - if(regex != null) { - obj.set("pattern",regex); + if (regex != null) { + obj.set("pattern", regex); } - if(enumValues != null) { + if (enumValues != null) { AbstractArray arr = new AbstractArray(enumValues.toArray()); - obj.set("enum",arr); + obj.set("enum", arr); } return obj; @@ -97,30 +97,30 @@ public AbstractObject toJsonSchema() { public List validate(AbstractPath path, AbstractElement value) { List errors = new ArrayList<>(); - if(value.getType() != AbstractElement.Type.STRING) { + if (value.getType() != AbstractElement.Type.STRING) { errors.add(new SchemaValidationError(path, "invalid_type").meta("expected", "string").meta("actual", value.getType().name().toLowerCase(Locale.ROOT))); return errors; } String s = value.string(); - if(staticValue != null && !staticValue.equals(s)) { + if (staticValue != null && !staticValue.equals(s)) { errors.add(new SchemaValidationError(path, "invalid_static_value").meta("expected", staticValue).meta("actual", s)); } - if(enumValues != null && !enumValues.contains(s)) { + if (enumValues != null && !enumValues.contains(s)) { errors.add(new SchemaValidationError(path, "invalid_enum_value").meta("expected", String.join(", ", enumValues)).meta("actual", s)); } - if(minLength != null && s.length() < minLength) { + if (minLength != null && s.length() < minLength) { errors.add(new SchemaValidationError(path, "value_too_short").meta("min", minLength.toString()).meta("actual", String.valueOf(s.length()))); } - if(maxLength != null && s.length() > maxLength) { + if (maxLength != null && s.length() > maxLength) { errors.add(new SchemaValidationError(path, "value_too_long").meta("max", maxLength.toString()).meta("actual", String.valueOf(s.length()))); } - if(regexPattern != null) { + if (regexPattern != null) { Matcher matcher = regexPattern.matcher(s); - if(!matcher.matches()) { + if (!matcher.matches()) { errors.add(new SchemaValidationError(path, "invalid_pattern").meta("pattern", regex).meta("actual", s)); } } - for(CustomValidation validation : customValidations) { + for (CustomValidation validation : customValidations) { errors.addAll(validation.validate(path, value.primitive())); } return errors; diff --git a/src/main/java/org/javawebstack/abstractdata/schema/JsonSchemaParser.java b/src/main/java/org/javawebstack/abstractdata/schema/JsonSchemaParser.java index 0eba0cf..7d2a3c1 100644 --- a/src/main/java/org/javawebstack/abstractdata/schema/JsonSchemaParser.java +++ b/src/main/java/org/javawebstack/abstractdata/schema/JsonSchemaParser.java @@ -4,7 +4,9 @@ import org.javawebstack.abstractdata.AbstractElement; import org.javawebstack.abstractdata.AbstractObject; -import java.util.*; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; /* { @@ -42,7 +44,7 @@ public static void main(String[] args) { } public AbstractSchema parse(AbstractObject schema) { - if(schema.has("type")) { + if (schema.has("type")) { switch (schema.string("type")) { case "object": { return parseObject(schema); @@ -65,44 +67,45 @@ public AbstractSchema parse(AbstractObject schema) { } } } - if(schema.has("$ref")) { + if (schema.has("$ref")) { throw new UnsupportedOperationException("$ref is currently not supported"); } - if(schema.has("oneOf")){ + if (schema.has("oneOf")) { return parseOneOf(schema); } throw new IllegalArgumentException("Invalid json schema"); } private OneOfSchema parseOneOf(AbstractObject schema) { - if(!schema.hasArray("oneOf")){ + if (!schema.hasArray("oneOf")) { throw new IllegalArgumentException("Not a valid oneOf schema"); } AbstractArray absArr = schema.array("oneOf"); AbstractSchema[] schemas = new AbstractSchema[absArr.size()]; - for(int i = 0; i(schema.array("enum").toStringList())); } - if(schema.has("const")){ + if (schema.has("const")) { s.staticValue(schema.string("const")); } return s; @@ -115,23 +118,23 @@ private AbstractNumberSchema parseNumber(AbstractObject schema) { throw new IllegalArgumentException("Not a valid number schema"); } AbstractNumberSchema s = new AbstractNumberSchema(); - if(isInteger){ + if (isInteger) { s.integerOnly(); } - if(schema.has("minimum")) + if (schema.has("minimum")) s.min(schema.number("minimum")); - if(schema.has("maximum")) + if (schema.has("maximum")) s.max(schema.number("maximum")); - if(schema.has("exclusiveMinimum")) - s.min(schema.number("exclusiveMinimum"),true); - if(schema.has("exclusiveMaximum")) - s.max(schema.number("exclusiveMaximum"),true); - if(schema.has("const")){ + if (schema.has("exclusiveMinimum")) + s.min(schema.number("exclusiveMinimum"), true); + if (schema.has("exclusiveMaximum")) + s.max(schema.number("exclusiveMaximum"), true); + if (schema.has("const")) { Number staticValue = schema.number("const"); s.min(staticValue); s.max(staticValue); } - if(schema.has("multipleOf")){ + if (schema.has("multipleOf")) { s.step(schema.number("multipleOf")); } @@ -140,13 +143,13 @@ private AbstractNumberSchema parseNumber(AbstractObject schema) { private AbstractObjectSchema parseObject(AbstractObject schema) { - if(!schema.string("type").equals("object")) + if (!schema.string("type").equals("object")) throw new IllegalArgumentException("Not a valid object schema"); AbstractObjectSchema s = new AbstractObjectSchema(); List required = schema.has("required") ? schema.array("required").toStringList() : Collections.emptyList(); - if(schema.has("properties")) { + if (schema.has("properties")) { schema.object("properties").forEach((key, propSchema) -> { - if(required.contains(key)) { + if (required.contains(key)) { s.requiredProperty(key, parse(propSchema.object())); } else { s.optionalProperty(key, parse(propSchema.object())); @@ -154,9 +157,9 @@ private AbstractObjectSchema parseObject(AbstractObject schema) { }); } AbstractElement additionalProperties = schema.get("additionalProperties"); - if(additionalProperties != null){ - if(additionalProperties.isBoolean()){ - if(additionalProperties.bool()){ + if (additionalProperties != null) { + if (additionalProperties.isBoolean()) { + if (additionalProperties.bool()) { s.additionalProperties(); } } else { @@ -171,29 +174,29 @@ private AbstractObjectSchema parseObject(AbstractObject schema) { } private AbstractArraySchema parseArray(AbstractObject schema) { - if(!schema.string("type").equals("array")) + if (!schema.string("type").equals("array")) throw new IllegalArgumentException("Not a valid array schema"); AbstractArraySchema s = new AbstractArraySchema(); - if(schema.has("items")) { + if (schema.has("items")) { s.itemSchema(parse(schema.object("items"))); } - if(schema.has("minItems")) { + if (schema.has("minItems")) { s.min(schema.number("minItems").intValue()); } - if(schema.has("maxItems")) { + if (schema.has("maxItems")) { s.max(schema.number("maxItems").intValue()); } - if(schema.has("uniqueItems") && schema.bool("uniqueItems")){ + if (schema.has("uniqueItems") && schema.bool("uniqueItems")) { s.unique(); } return s; } - private AbstractBooleanSchema parseBoolean(AbstractObject schema){ - if(!schema.string("type").equals("boolean")) + private AbstractBooleanSchema parseBoolean(AbstractObject schema) { + if (!schema.string("type").equals("boolean")) throw new IllegalArgumentException("Not a valid boolean schema"); AbstractBooleanSchema s = new AbstractBooleanSchema(); - if(schema.has("const")){ + if (schema.has("const")) { s.staticValue(schema.bool("const")); } diff --git a/src/main/java/org/javawebstack/abstractdata/schema/OneOfSchema.java b/src/main/java/org/javawebstack/abstractdata/schema/OneOfSchema.java index 556b326..ce4611e 100644 --- a/src/main/java/org/javawebstack/abstractdata/schema/OneOfSchema.java +++ b/src/main/java/org/javawebstack/abstractdata/schema/OneOfSchema.java @@ -14,7 +14,7 @@ public class OneOfSchema implements AbstractSchema { private final List schemas = new ArrayList<>(); public OneOfSchema(AbstractSchema... schemas) { - if(schemas.length == 0) + if (schemas.length == 0) throw new IllegalArgumentException("At least one schema is required"); this.schemas.addAll(Arrays.asList(schemas)); } @@ -27,19 +27,19 @@ public AbstractObject toJsonSchema() { } - return new AbstractObject().set("oneOf",arr); + return new AbstractObject().set("oneOf", arr); } public List validate(AbstractPath path, AbstractElement value) { List> schemaErrors = new ArrayList<>(); - for(AbstractSchema schema : schemas) { + for (AbstractSchema schema : schemas) { List errors = schema.validate(path, value); - if(errors.isEmpty()) + if (errors.isEmpty()) return errors; schemaErrors.add(errors); } - for(List errors : schemaErrors) { - if(!(errors.size() == 1 && errors.get(0).getError().equals("invalid_type"))) + for (List errors : schemaErrors) { + if (!(errors.size() == 1 && errors.get(0).getError().equals("invalid_type"))) return errors; } return schemaErrors.get(0); diff --git a/src/main/java/org/javawebstack/abstractdata/schema/SchemaValidationError.java b/src/main/java/org/javawebstack/abstractdata/schema/SchemaValidationError.java index 68f0dee..fd6d018 100644 --- a/src/main/java/org/javawebstack/abstractdata/schema/SchemaValidationError.java +++ b/src/main/java/org/javawebstack/abstractdata/schema/SchemaValidationError.java @@ -58,14 +58,14 @@ public String getErrorDescription() { public String getErrorDescription(Map customDescriptions) { String message; - if(customDescriptions.containsKey(error)) { + if (customDescriptions.containsKey(error)) { message = customDescriptions.get(error); - } else if(BUILTIN_DESCRIPTIONS.containsKey(error)) { + } else if (BUILTIN_DESCRIPTIONS.containsKey(error)) { message = BUILTIN_DESCRIPTIONS.get(error); } else { return error; } - for(String key : errorMeta.keySet()) { + for (String key : errorMeta.keySet()) { message = message.replace("{" + key + "}", errorMeta.get(key)); } return message; @@ -73,7 +73,7 @@ public String getErrorDescription(Map customDescriptions) { public static Map> groupErrors(List errors) { Map> errorMap = new HashMap<>(); - for(SchemaValidationError e : errors) { + for (SchemaValidationError e : errors) { errorMap.computeIfAbsent(e.getPath(), k -> new ArrayList<>()).add(e); } return errorMap; diff --git a/src/main/java/org/javawebstack/abstractdata/util/Helpers.java b/src/main/java/org/javawebstack/abstractdata/util/Helpers.java index 4247a9e..856289a 100644 --- a/src/main/java/org/javawebstack/abstractdata/util/Helpers.java +++ b/src/main/java/org/javawebstack/abstractdata/util/Helpers.java @@ -10,29 +10,29 @@ public class Helpers { public static String typeName(AbstractElement e) { - if(e.isArray()) + if (e.isArray()) return "array"; - if(e.isObject()) + if (e.isObject()) return "object"; - if(e.isString()) + if (e.isString()) return "string"; - if(e.isNumber()) + if (e.isNumber()) return "number"; - if(e.isBoolean()) + if (e.isBoolean()) return "boolean"; return "null"; } public static Class guessGeneric(AbstractElement e) { - if(e.isArray()) + if (e.isArray()) return AbstractArray.class; - if(e.isObject()) + if (e.isObject()) return AbstractObject.class; - if(e.isString()) + if (e.isString()) return String.class; - if(e.isNumber()) + if (e.isNumber()) return Number.class; - if(e.isBoolean()) + if (e.isBoolean()) return Boolean.class; return null; } @@ -40,21 +40,21 @@ public static Class guessGeneric(AbstractElement e) { public static List words(String s) { List words = new ArrayList<>(); StringBuilder sb = new StringBuilder(); - for(int i=0; i 0) { + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) == '_' || s.charAt(i) == '-') { + if (sb.length() > 0) { words.add(sb.toString()); sb = new StringBuilder(); } continue; } - if(i > 0 && (Character.isLowerCase(s.charAt(i - 1)) && Character.isUpperCase(s.charAt(i)))) { + if (i > 0 && (Character.isLowerCase(s.charAt(i - 1)) && Character.isUpperCase(s.charAt(i)))) { words.add(sb.toString()); sb = new StringBuilder(); } sb.append(Character.toLowerCase(s.charAt(i))); } - if(sb.length() > 0) + if (sb.length() > 0) words.add(sb.toString()); return words; } diff --git a/src/main/java/org/javawebstack/abstractdata/xml/LegacyXMLParser.java b/src/main/java/org/javawebstack/abstractdata/xml/LegacyXMLParser.java index ddde196..b7077af 100644 --- a/src/main/java/org/javawebstack/abstractdata/xml/LegacyXMLParser.java +++ b/src/main/java/org/javawebstack/abstractdata/xml/LegacyXMLParser.java @@ -28,13 +28,13 @@ private static XMLElement convert(Element element) { String tag = element.getTagName(); XMLElement e = new XMLElement(tag); NamedNodeMap attrNodeMap = element.getAttributes(); - for(int i=0; i dumpLines(XMLNode node) { - if(node instanceof XMLTextNode) { + if (node instanceof XMLTextNode) { XMLTextNode textNode = (XMLTextNode) node; return Arrays.asList(escape(textNode.getText(), true)); } @@ -41,14 +41,14 @@ private List dumpLines(XMLNode node) { boolean selfClosing = useSelfClosing && element.getChildNodes().size() == 0; String openingTag = renderOpeningTag(element.tagName(), element.getAttributes(), selfClosing); String closingTag = selfClosing ? "" : ""; - if(pretty) { - if(element.getChildNodes().size() == 0) { + if (pretty) { + if (element.getChildNodes().size() == 0) { lines.add(openingTag + closingTag); - } else if(element.getChildNodes().size() == 1 && element.getChildNodes().get(0) instanceof XMLTextNode) { + } else if (element.getChildNodes().size() == 1 && element.getChildNodes().get(0) instanceof XMLTextNode) { lines.add(openingTag + escape(((XMLTextNode) element.getChildNodes().get(0)).getText(), true) + closingTag); } else { lines.add(openingTag); - for(XMLNode child : element.getChildNodes()) { + for (XMLNode child : element.getChildNodes()) { lines.addAll(dumpLines(child).stream().map(l -> indent + l).collect(Collectors.toList())); } lines.add(closingTag); @@ -56,7 +56,7 @@ private List dumpLines(XMLNode node) { } else { StringBuilder sb = new StringBuilder(); sb.append(openingTag); - for(XMLNode child : element.getChildNodes()) + for (XMLNode child : element.getChildNodes()) dumpLines(child).forEach(sb::append); sb.append(closingTag); lines.add(sb.toString()); @@ -68,7 +68,7 @@ public String renderOpeningTag(String tagName, Map attributes, b StringBuilder sb = new StringBuilder(); sb.append('<'); sb.append(escape(tagName, false)); - for(String attrName : attributes.keySet()) { + for (String attrName : attributes.keySet()) { sb.append(" "); sb.append(escape(attrName, false)); sb.append('='); @@ -76,7 +76,7 @@ public String renderOpeningTag(String tagName, Map attributes, b sb.append(escape(attributes.get(attrName), false)); sb.append('"'); } - if(selfClosing) + if (selfClosing) sb.append("/"); sb.append('>'); return sb.toString(); @@ -85,7 +85,7 @@ public String renderOpeningTag(String tagName, Map attributes, b private String escape(String value, boolean text) { value = value.replace("<", "<"); value = value.replace("&", "&"); - if(!text) { + if (!text) { value = value.replace("\"", """); value = value.replace("'", "'"); value = value.replace(">", ">"); diff --git a/src/main/java/org/javawebstack/abstractdata/xml/XMLElement.java b/src/main/java/org/javawebstack/abstractdata/xml/XMLElement.java index c9771ab..fed5f99 100644 --- a/src/main/java/org/javawebstack/abstractdata/xml/XMLElement.java +++ b/src/main/java/org/javawebstack/abstractdata/xml/XMLElement.java @@ -14,14 +14,14 @@ public class XMLElement implements XMLNode { private final List childNodes = new ArrayList<>(); public XMLElement(String tagName) { - if(tagName == null) + if (tagName == null) throw new IllegalArgumentException("tagName can not be null"); this.tagName = tagName; } public XMLElement(String tagName, String text) { this(tagName); - if(text == null) + if (text == null) throw new IllegalArgumentException("text can not be null"); text(text); } @@ -54,7 +54,7 @@ public String attr(String name) { } public XMLElement attr(String name, String value) { - if(value == null) { + if (value == null) { attributes.remove(name); return this; } @@ -78,7 +78,7 @@ public XMLElement child(XMLNode childNode) { } public XMLElement onlyIf(boolean condition, Consumer fn) { - if(condition) + if (condition) fn.accept(this); return this; } From 3684fde56a259920e2f9e0e3adeace83a5643150 Mon Sep 17 00:00:00 2001 From: JanHolger Date: Sat, 9 Mar 2024 16:29:10 +0100 Subject: [PATCH 09/19] Improved test coverage in AbstractArray, did a few minor fixes to prevent rare but unexpected NPE's, deprecated the typed query helpers in AbstractArray as they have barely any use-case and unnecessarily bloat the code --- .../abstractdata/AbstractArray.java | 75 +++++-- .../abstractdata/AbstractArrayTest.java | 189 ++++++++++++++++++ 2 files changed, 246 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/javawebstack/abstractdata/AbstractArray.java b/src/main/java/org/javawebstack/abstractdata/AbstractArray.java index a94faaa..c92fa98 100644 --- a/src/main/java/org/javawebstack/abstractdata/AbstractArray.java +++ b/src/main/java/org/javawebstack/abstractdata/AbstractArray.java @@ -30,76 +30,109 @@ public AbstractObject object(boolean strict) throws AbstractCoercingException { return object; } + @Deprecated public AbstractObject object(String key) throws AbstractCoercingException { - return query(key).object(); + AbstractElement e = query(key); + return e != null ? e.object() : null; } + @Deprecated public AbstractArray array(String key) throws AbstractCoercingException { - return query(key).array(); + AbstractElement e = query(key); + return e != null ? e.array() : null; } + @Deprecated public AbstractPrimitive primitive(String key) throws AbstractCoercingException { - return query(key).primitive(); + AbstractElement e = query(key); + return e != null ? e.primitive() : null; } + @Deprecated public String string(String key) throws AbstractCoercingException { - return query(key).string(); + AbstractElement e = query(key); + return e != null ? e.string() : null; } + @Deprecated public Boolean bool(String key) throws AbstractCoercingException { - return query(key).bool(); + AbstractElement e = query(key); + return e != null ? e.bool() : null; } + @Deprecated public Number number(String key) throws AbstractCoercingException { - return query(key).number(); + AbstractElement e = query(key); + return e != null ? e.number() : null; } + @Deprecated public AbstractObject object(String key, AbstractObject orElse) throws AbstractCoercingException { - return query(key, orElse).object(); + AbstractElement e = query(key, orElse); + return e != null ? e.object() : null; } + @Deprecated public AbstractArray array(String key, AbstractArray orElse) throws AbstractCoercingException { - return query(key, orElse).array(); + AbstractElement e = query(key, orElse); + return e != null ? e.array() : null; } + @Deprecated public AbstractPrimitive primitive(String key, AbstractPrimitive orElse) throws AbstractCoercingException { - return query(key, orElse).primitive(); + AbstractElement e = query(key, orElse); + return e != null ? e.primitive() : null; } + @Deprecated public String string(String key, String orElse) throws AbstractCoercingException { - return query(key, new AbstractPrimitive(orElse)).string(); + AbstractPrimitive orElsePrimitive = orElse != null ? new AbstractPrimitive(orElse) : null; + AbstractElement e = primitive(key, orElsePrimitive); + return e != null ? e.string() : null; } + @Deprecated public Boolean bool(String key, Boolean orElse) throws AbstractCoercingException { - return query(key, new AbstractPrimitive(orElse)).bool(); + AbstractPrimitive orElsePrimitive = orElse != null ? new AbstractPrimitive(orElse) : null; + AbstractElement e = primitive(key, orElsePrimitive); + return e != null ? e.bool() : null; } + @Deprecated public Number number(String key, Number orElse) throws AbstractCoercingException { - return query(key, new AbstractPrimitive(orElse)).number(); + AbstractPrimitive orElsePrimitive = orElse != null ? new AbstractPrimitive(orElse) : null; + AbstractElement e = primitive(key, orElsePrimitive); + return e != null ? e.number() : null; } public AbstractObject object(int index) throws AbstractCoercingException { - return get(index).object(); + AbstractElement e = get(index); + return e != null ? e.object() : null; } public AbstractArray array(int index) throws AbstractCoercingException { - return get(index).array(); + AbstractElement e = get(index); + return e != null ? e.array() : null; } public AbstractPrimitive primitive(int index) throws AbstractCoercingException { - return get(index).primitive(); + AbstractElement e = get(index); + return e != null ? e.primitive() : null; } public String string(int index) throws AbstractCoercingException { - return get(index).string(); + AbstractElement e = get(index); + return e != null ? e.string() : null; } public Boolean bool(int index) throws AbstractCoercingException { - return get(index).bool(); + AbstractElement e = get(index); + return e != null ? e.bool() : null; } public Number number(int index) throws AbstractCoercingException { - return get(index).number(); + AbstractElement e = get(index); + return e != null ? e.number() : null; } public AbstractObject object(int index, AbstractObject orElse) throws AbstractCoercingException { @@ -214,6 +247,8 @@ public AbstractElement[] toArray() { } public AbstractElement get(int i) { + if(i >= elements.size()) + return null; return elements.get(i); } @@ -223,6 +258,8 @@ public AbstractElement get(int index, AbstractElement orElse) { } public AbstractElement query(String query) { + if(query == null) + throw new IllegalArgumentException("query can not be null"); String[] q = query.split("\\.", 2); try { int index = Integer.parseInt(q[0]); @@ -370,6 +407,8 @@ public boolean equals(Object obj) { } public AbstractArray addAll(AbstractArray array) { + if(array == null) + throw new IllegalArgumentException("array may not be null"); elements.addAll(array.elements); return this; } diff --git a/src/test/java/org/javawebstack/abstractdata/AbstractArrayTest.java b/src/test/java/org/javawebstack/abstractdata/AbstractArrayTest.java index d02306f..0c503f9 100644 --- a/src/test/java/org/javawebstack/abstractdata/AbstractArrayTest.java +++ b/src/test/java/org/javawebstack/abstractdata/AbstractArrayTest.java @@ -1,11 +1,200 @@ package org.javawebstack.abstractdata; +import org.javawebstack.abstractdata.exception.AbstractCoercingException; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class AbstractArrayTest { + @Test + void testIsArrayReturnsTrue() { + assertTrue(new AbstractArray().isArray()); + } + + @Test + void testObjectThrowsInStrictMode() { + assertThrows(AbstractCoercingException.class, () -> new AbstractArray().object(true)); + } + + @Test + void testObjectCoercingWhenNotInStrictMode() { + AbstractArray a = new AbstractArray(); + a.add(0); + a.add("1"); + AbstractObject o = assertDoesNotThrow(() -> a.object(false)); + assertTrue(o.hasNumber("0")); + assertTrue(o.hasString("1")); + } + + @Test + void testObjectQueryReturnsNullWhenKeyIsNotFound() { + assertNull(new AbstractArray().add(new AbstractObject()).object("1")); + } + + @Test + void testObjectQueryReturnsValueWhenKeyIsFound() { + assertNotNull(new AbstractArray().add(new AbstractObject()).object("0")); + } + + @Test + void testArrayQueryReturnsNullWhenKeyIsNotFound() { + assertNull(new AbstractArray().add(new AbstractArray()).array("1")); + } + + @Test + void testArrayQueryReturnsValueWhenKeyIsFound() { + assertNotNull(new AbstractArray().add(new AbstractArray()).array("0")); + } + + @Test + void testPrimitiveQueryReturnsNullWhenKeyIsNotFound() { + assertNull(new AbstractArray().add(0).primitive("1")); + } + + @Test + void testPrimitiveQueryReturnsValueWhenKeyIsFound() { + assertNotNull(new AbstractArray().add(0).primitive("0")); + } + + @Test + void testStringQueryReturnsNullWhenKeyIsNotFound() { + assertNull(new AbstractArray().add("abc").string("1")); + } + + @Test + void testStringQueryReturnsValueWhenKeyIsFound() { + assertEquals("abc", new AbstractArray().add("abc").string("0")); + } + + @Test + void testNumberQueryReturnsNullWhenKeyIsNotFound() { + assertNull(new AbstractArray().add(123).number("1")); + } + + @Test + void testNumberQueryReturnsValueWhenKeyIsFound() { + assertEquals(123, new AbstractArray().add(123).number("0")); + } + + @Test + void testBoolQueryReturnsNullWhenKeyIsNotFound() { + assertNull(new AbstractArray().add(true).bool("1")); + } + + @Test + void testBoolQueryReturnsValueWhenKeyIsFound() { + assertEquals(true, new AbstractArray().add(true).bool("0")); + } + + @Test + void testQueryOrElseReturnsValueIfFoundAndNonNull() { + AbstractElement expected = new AbstractPrimitive(123); + AbstractElement orElse = new AbstractPrimitive("456"); + AbstractArray array = new AbstractArray().add(expected); + assertEquals(expected, array.query("0", orElse)); + } + + @Test + void testQueryOrElseReturnsOrElseIfFoundAndNull() { + AbstractElement orElse = new AbstractPrimitive("456"); + AbstractArray array = new AbstractArray().addNull(); + assertEquals(orElse, array.query("0", orElse)); + } + + @Test + void testQueryOrElseReturnsOrElseIfNotFound() { + AbstractElement unexpected = new AbstractPrimitive(123); + AbstractElement orElse = new AbstractPrimitive("456"); + AbstractArray array = new AbstractArray().add(unexpected); + assertEquals(orElse, array.query("1", orElse)); + } + + @Test + void testObjectQueryOrElseReturnsOrElseIfNotFound() { + AbstractObject orElse = new AbstractObject().set("a", 1); + assertSame(orElse, new AbstractArray().object("0", orElse)); + } + + @Test + void testArrayQueryOrElseReturnsOrElseIfNotFound() { + AbstractArray orElse = new AbstractArray().add(123); + assertSame(orElse, new AbstractArray().array("0", orElse)); + } + + @Test + void testPrimitiveQueryOrElseReturnsOrElseIfNotFound() { + AbstractPrimitive orElse = new AbstractPrimitive(123); + assertSame(orElse, new AbstractArray().primitive("0", orElse)); + } + + @Test + void testStringQueryOrElseReturnsOrElseIfNotFound() { + String orElse = "abc"; + assertEquals(orElse, new AbstractArray().string("0", orElse)); + } + + @Test + void testNumberQueryOrElseReturnsOrElseIfNotFound() { + Number orElse = 123; + assertEquals(orElse, new AbstractArray().number("0", orElse)); + } + + @Test + void testBoolQueryOrElseReturnsOrElseIfNotFound() { + Boolean orElse = true; + assertEquals(orElse, new AbstractArray().bool("0", orElse)); + } + + @Test + void testQueryThrowsOnNull() { + assertThrows(IllegalArgumentException.class, () -> new AbstractArray().query(null)); + } + + @Test + void testQueryWithNonIntPathReturnsNull() { + assertNull(new AbstractArray().add(1).query("abc")); + } + + @Test + void testQueryWithNonExistingIndexReturnsNull() { + assertNull(new AbstractArray().add(0).query("1")); + } + + @Test + void testQueryWithOnlyOneSegmentReturnsValue() { + assertNotNull(new AbstractArray().add(1).query("0")); + } + + @Test + void testClearClearsTheArrayAndReturnsThis() { + AbstractArray array = new AbstractArray().add(1).add(2).add(3); + assertEquals(3, array.size()); + assertSame(array, array.clear()); + assertEquals(0, array.size()); + } + + @Test + void testQueryWithObjectValueQueriesObject() { + AbstractArray array = new AbstractArray(); + array.add(new AbstractObject().set("a", 1)); + assertEquals(1, array.query("0.a").number().intValue()); + } + + @Test + void testQueryWithArrayValueQueriesArray() { + AbstractArray array = new AbstractArray(); + array.add(new AbstractArray().add(1)); + assertEquals(1, array.query("0.0").number().intValue()); + } + + @Test + void testQueryWithPrimitiveValueReturnsNull() { + AbstractArray array = new AbstractArray(); + array.add("abc"); + assertNull(array.query("0.abc")); + } + @Test void testEqualsNormal() { AbstractArray first = new AbstractArray(); From fc8db8b5cb2d06dee068df623aa261ab1258aab1 Mon Sep 17 00:00:00 2001 From: JanHolger Date: Sun, 10 Mar 2024 13:35:34 +0100 Subject: [PATCH 10/19] Implemented test for collectors and JsonDumper --- pom.xml | 6 + .../collector/AbstractArrayCollector.java | 1 - .../abstractdata/json/JsonDumper.java | 2 +- .../collector/AbstractArrayCollectorTest.java | 20 +++ .../AbstractObjectCollectorTest.java | 21 ++++ .../abstractdata/json/JsonDumperTest.java | 116 ++++++++++++++++++ 6 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 src/test/java/org/javawebstack/abstractdata/collector/AbstractArrayCollectorTest.java create mode 100644 src/test/java/org/javawebstack/abstractdata/collector/AbstractObjectCollectorTest.java create mode 100644 src/test/java/org/javawebstack/abstractdata/json/JsonDumperTest.java diff --git a/pom.xml b/pom.xml index e157e32..d012168 100644 --- a/pom.xml +++ b/pom.xml @@ -58,6 +58,12 @@ 5.10.0 test + + org.mockito + mockito-core + 4.11.0 + test + diff --git a/src/main/java/org/javawebstack/abstractdata/collector/AbstractArrayCollector.java b/src/main/java/org/javawebstack/abstractdata/collector/AbstractArrayCollector.java index f9cd104..07b2d76 100644 --- a/src/main/java/org/javawebstack/abstractdata/collector/AbstractArrayCollector.java +++ b/src/main/java/org/javawebstack/abstractdata/collector/AbstractArrayCollector.java @@ -46,7 +46,6 @@ public Function, AbstractArray> finisher() { public Set characteristics() { Set characteristics = new HashSet<>(); - characteristics.add(Characteristics.UNORDERED); return characteristics; } diff --git a/src/main/java/org/javawebstack/abstractdata/json/JsonDumper.java b/src/main/java/org/javawebstack/abstractdata/json/JsonDumper.java index a8097f3..f33abf5 100644 --- a/src/main/java/org/javawebstack/abstractdata/json/JsonDumper.java +++ b/src/main/java/org/javawebstack/abstractdata/json/JsonDumper.java @@ -83,7 +83,7 @@ private List dumpLines(AbstractElement element) { } return lines; } - return new ArrayList<>(); + throw new IllegalArgumentException("Unknown element type"); // Should be unreachable } private static String escape(String s) { diff --git a/src/test/java/org/javawebstack/abstractdata/collector/AbstractArrayCollectorTest.java b/src/test/java/org/javawebstack/abstractdata/collector/AbstractArrayCollectorTest.java new file mode 100644 index 0000000..1d1c4a5 --- /dev/null +++ b/src/test/java/org/javawebstack/abstractdata/collector/AbstractArrayCollectorTest.java @@ -0,0 +1,20 @@ +package org.javawebstack.abstractdata.collector; + +import org.javawebstack.abstractdata.AbstractArray; +import org.javawebstack.abstractdata.AbstractPrimitive; +import org.junit.jupiter.api.Test; +import java.util.stream.Stream; +import static org.junit.jupiter.api.Assertions.*; + +public class AbstractArrayCollectorTest { + + @Test + public void testCollection() { + AbstractArray array = Stream.of("a", "b", "c").parallel().collect(new AbstractArrayCollector<>(AbstractPrimitive::new)); + assertEquals(3, array.size()); + assertEquals("a", array.string(0)); + assertEquals("b", array.string(1)); + assertEquals("c", array.string(2)); + } + +} diff --git a/src/test/java/org/javawebstack/abstractdata/collector/AbstractObjectCollectorTest.java b/src/test/java/org/javawebstack/abstractdata/collector/AbstractObjectCollectorTest.java new file mode 100644 index 0000000..c79416a --- /dev/null +++ b/src/test/java/org/javawebstack/abstractdata/collector/AbstractObjectCollectorTest.java @@ -0,0 +1,21 @@ +package org.javawebstack.abstractdata.collector; + +import org.javawebstack.abstractdata.AbstractObject; +import org.javawebstack.abstractdata.AbstractPrimitive; +import org.junit.jupiter.api.Test; + +import java.util.stream.Stream; +import static org.junit.jupiter.api.Assertions.*; + +public class AbstractObjectCollectorTest { + + @Test + public void testCollection() { + AbstractObject object = Stream.of("a", "bc", "def").parallel().collect(new AbstractObjectCollector<>(s -> s, s -> new AbstractPrimitive(s.length()))); + assertEquals(3, object.size()); + assertEquals(1, object.number("a")); + assertEquals(2, object.number("bc")); + assertEquals(3, object.number("def")); + } + +} diff --git a/src/test/java/org/javawebstack/abstractdata/json/JsonDumperTest.java b/src/test/java/org/javawebstack/abstractdata/json/JsonDumperTest.java new file mode 100644 index 0000000..1134be7 --- /dev/null +++ b/src/test/java/org/javawebstack/abstractdata/json/JsonDumperTest.java @@ -0,0 +1,116 @@ +package org.javawebstack.abstractdata.json; + +import org.javawebstack.abstractdata.*; +import org.junit.jupiter.api.Test; +import static org.mockito.Mockito.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.*; + +public class JsonDumperTest { + + @Test + public void testDumpJavaNull() { + assertEquals("null", new JsonDumper().dump(null)); + } + + @Test + public void testDumpNull() { + assertEquals("null", new JsonDumper().dump(AbstractNull.VALUE)); + } + + @Test + public void testDumpString() { + assertEquals("\"abc\"", new JsonDumper().dump(new AbstractPrimitive("abc"))); + } + + @Test + public void testDumpNumber() { + assertEquals("123.456", new JsonDumper().dump(new AbstractPrimitive(123.456))); + } + + @Test + public void testDumpBoolean() { + assertEquals("true", new JsonDumper().dump(new AbstractPrimitive(true))); + assertEquals("false", new JsonDumper().dump(new AbstractPrimitive(false))); + } + + @Test + public void testDumpObject() { + assertEquals("{\"a\":1,\"b\":2}", new JsonDumper().dump(new AbstractObject().set("a", 1).set("b", 2))); + } + + @Test + public void testDumpArray() { + assertEquals("[1,2,3]", new JsonDumper().dump(new AbstractArray().add(1).add(2).add(3))); + } + + @Test + public void testEscapeStringEscapeSeq() { + Map escapes = new HashMap<>(); + escapes.put("\"", "\\\""); + escapes.put("\\", "\\\\"); + escapes.put("\b", "\\b"); + escapes.put("\f", "\\f"); + escapes.put("\n", "\\n"); + escapes.put("\r", "\\r"); + escapes.put("\t", "\\t"); + escapes.put("\0", "\\0"); + escapes.put("/", "\\/"); + for(String c : escapes.keySet()) { + assertEquals("\"" + escapes.get(c) + "\"", new JsonDumper().dump(new AbstractPrimitive(c))); + } + } + + @Test + public void testEscapeStringUnicode() { + assertEquals("\"\\u001F\"", new JsonDumper().dump(new AbstractPrimitive("\u001F"))); + } + + @Test + public void testPrettyFlag() { + AbstractObject o = new AbstractObject() + .set("a", 1) + .set("b", new AbstractArray().add("a").add("b")); + String expectedNonPretty = "{\"a\":1,\"b\":[\"a\",\"b\"]}"; + String expectedPretty = "{\n \"a\": 1,\n \"b\": [\n \"a\",\n \"b\"\n ]\n}"; + assertEquals(expectedNonPretty, new JsonDumper().dump(o)); + assertEquals(expectedPretty, new JsonDumper().setPretty(true).dump(o)); + } + + @Test + public void testIndent() { + AbstractObject o = new AbstractObject() + .set("a", 1) + .set("b", new AbstractArray().add("a").add("b")); + String defaultIndent = " "; + String shortIndent = " "; + Function expectedFn = indent -> "{\n" + indent + "\"a\": 1,\n" + indent + "\"b\": [\n" + indent + indent + "\"a\",\n" + indent + indent + "\"b\"\n" + indent + "]\n}"; + assertEquals(expectedFn.apply(defaultIndent), new JsonDumper().setPretty(true).dump(o)); + assertEquals(expectedFn.apply(shortIndent), new JsonDumper().setIndent(shortIndent).setPretty(true).dump(o)); + } + + @Test + public void testNoNewlineOnEmptyObject() { + AbstractObject object = new AbstractObject().set("a", new AbstractObject()); + String expected = "{\n \"a\": {}\n}"; + assertEquals(expected, new JsonDumper().setPretty(true).dump(object)); + } + + @Test + public void testNoNewlineOnEmptyArray() { + AbstractObject object = new AbstractObject().set("a", new AbstractArray()); + String expected = "{\n \"a\": []\n}"; + assertEquals(expected, new JsonDumper().setPretty(true).dump(object)); + } + + @Test + public void testDumpUnknownType() { + AbstractElement e = mock(AbstractElement.class); + assertThrows(IllegalArgumentException.class, () -> new JsonDumper().dump(e)); + } + +} From 64444aae92237a85229086f86fdddbe083fb314e Mon Sep 17 00:00:00 2001 From: JanHolger Date: Sun, 10 Mar 2024 15:55:23 +0100 Subject: [PATCH 11/19] Implemented test for JsonParser --- .../abstractdata/json/JsonParser.java | 25 +-- .../abstractdata/json/JsonParserTest.java | 169 ++++++++++++++++++ 2 files changed, 182 insertions(+), 12 deletions(-) create mode 100644 src/test/java/org/javawebstack/abstractdata/json/JsonParserTest.java diff --git a/src/main/java/org/javawebstack/abstractdata/json/JsonParser.java b/src/main/java/org/javawebstack/abstractdata/json/JsonParser.java index 1afedd2..980a7f0 100644 --- a/src/main/java/org/javawebstack/abstractdata/json/JsonParser.java +++ b/src/main/java/org/javawebstack/abstractdata/json/JsonParser.java @@ -3,10 +3,7 @@ import org.javawebstack.abstractdata.*; import java.text.ParseException; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Deque; -import java.util.List; +import java.util.*; public class JsonParser { @@ -19,7 +16,7 @@ public AbstractElement parse(String json) throws ParseException { AbstractElement parsed; try { parsed = parse(stack); - } catch (NullPointerException ex) { + } catch (NoSuchElementException | NullPointerException ex) { throw new ParseException("Unexpected character ", primChars.length); } if (parsed == null) { @@ -28,11 +25,11 @@ public AbstractElement parse(String json) throws ParseException { for (int i = 0; i < primChars.length - stack.size(); i++) { if (primChars[i] == '\n') { line++; - pos = 1; + pos = 0; } pos++; } - throw new ParseException("Unexpected character '" + stack.pop() + "' at line " + line + " pos " + pos, primChars.length - stack.size()); + throw new ParseException("Unexpected character '" + stack.pop() + "' at line " + line + " pos " + pos, primChars.length - stack.size() - 1); } return parsed; } @@ -114,7 +111,7 @@ private void popWhitespace(Deque stack) { private AbstractPrimitive parseNumber(Deque stack) { StringBuilder sb = new StringBuilder(); - while (Character.isDigit(stack.peek()) || stack.peek() == '.' || stack.peek() == '-' || stack.peek() == 'E' || stack.peek() == 'e') + while (stack.peek() != null && (Character.isDigit(stack.peek()) || stack.peek() == '.' || stack.peek() == '+' || stack.peek() == '-' || stack.peek() == 'E' || stack.peek() == 'e')) sb.append(stack.pop()); String s = sb.toString(); if (s.contains(".")) { @@ -181,8 +178,6 @@ private AbstractObject parseObject(Deque stack) { break; } AbstractPrimitive key = parseString(stack); - if (key == null) - return null; popWhitespace(stack); if (stack.peek() != ':') return null; @@ -193,8 +188,11 @@ private AbstractObject parseObject(Deque stack) { return null; object.set(key.string(), value); popWhitespace(stack); - if (stack.peek() == ',') + if (stack.peek() == ',') { stack.pop(); + } else if(stack.peek() != '}') { + return null; + } } return object; } @@ -213,8 +211,11 @@ private AbstractArray parseArray(Deque stack) { return null; array.add(value); popWhitespace(stack); - if (stack.peek() == ',') + if (stack.peek() == ',') { stack.pop(); + } else if(stack.peek() != ']') { + return null; + } } return array; } diff --git a/src/test/java/org/javawebstack/abstractdata/json/JsonParserTest.java b/src/test/java/org/javawebstack/abstractdata/json/JsonParserTest.java new file mode 100644 index 0000000..5c96faa --- /dev/null +++ b/src/test/java/org/javawebstack/abstractdata/json/JsonParserTest.java @@ -0,0 +1,169 @@ +package org.javawebstack.abstractdata.json; + +import org.javawebstack.abstractdata.AbstractElement; +import org.javawebstack.abstractdata.AbstractPrimitive; +import org.junit.jupiter.api.Test; + +import java.text.ParseException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +public class JsonParserTest { + + @Test + public void testParseEmptyString() { + ParseException e = assertThrows(ParseException.class, () -> new JsonParser().parse("")); + assertEquals("Unexpected character ", e.getMessage()); + } + + @Test + public void testParseUnexpectedEOF() { + ParseException e = assertThrows(ParseException.class, () -> new JsonParser().parse("{")); + assertEquals("Unexpected character ", e.getMessage()); + e = assertThrows(ParseException.class, () -> new JsonParser().parse("[")); + assertEquals("Unexpected character ", e.getMessage()); + e = assertThrows(ParseException.class, () -> new JsonParser().parse("\"")); + assertEquals("Unexpected character ", e.getMessage()); + e = assertThrows(ParseException.class, () -> new JsonParser().parse("t")); + assertEquals("Unexpected character ", e.getMessage()); + } + + @Test + public void testParseUnexpectedCharacter() { + ParseException e = assertThrows(ParseException.class, () -> new JsonParser().parse("{\n \"abc\":x\n}")); + assertEquals("Unexpected character 'x' at line 2 pos 11", e.getMessage()); + assertEquals(12, e.getErrorOffset()); + } + + @Test + public void testParseBooleanTrue() { + AbstractElement trueElement = assertDoesNotThrow(() -> new JsonParser().parse("true")); + assertTrue(trueElement.isBoolean()); + assertTrue(trueElement.bool()); + assertThrows(ParseException.class, () -> new JsonParser().parse("trux")); + assertThrows(ParseException.class, () -> new JsonParser().parse("trxx")); + assertThrows(ParseException.class, () -> new JsonParser().parse("txxx")); + } + + @Test + public void testParseBooleanFalse() { + AbstractElement falseElement = assertDoesNotThrow(() -> new JsonParser().parse("false")); + assertTrue(falseElement.isBoolean()); + assertFalse(falseElement.bool()); + assertThrows(ParseException.class, () -> new JsonParser().parse("falsx")); + assertThrows(ParseException.class, () -> new JsonParser().parse("falxx")); + assertThrows(ParseException.class, () -> new JsonParser().parse("faxxx")); + assertThrows(ParseException.class, () -> new JsonParser().parse("fxxxx")); + } + + @Test + public void testParseBooleanNull() { + AbstractElement nullElement = assertDoesNotThrow(() -> new JsonParser().parse("null")); + assertTrue(nullElement.isNull()); + assertThrows(ParseException.class, () -> new JsonParser().parse("nulx")); + assertThrows(ParseException.class, () -> new JsonParser().parse("nuxx")); + assertThrows(ParseException.class, () -> new JsonParser().parse("nxxx")); + } + + @Test + public void testParseInteger() { + AbstractElement e = assertDoesNotThrow(() -> new JsonParser().parse("123")); + assertTrue(e.isNumber()); + assertEquals(123, e.number()); + } + + @Test + public void testParseLong() { + long expected = 1L + Integer.MAX_VALUE; + AbstractElement e = assertDoesNotThrow(() -> new JsonParser().parse(String.valueOf(expected))); + assertTrue(e.isNumber()); + assertEquals(expected, e.number()); + } + + @Test + public void testParseDouble() { + AbstractElement e = assertDoesNotThrow(() -> new JsonParser().parse("123.456")); + assertTrue(e.isNumber()); + assertEquals(123.456, e.number()); + } + + @Test + public void testParseStringEscapeSeq() { + Map escapes = new HashMap<>(); + escapes.put("\\\"", "\""); + escapes.put("\\\\", "\\"); + escapes.put("\\b", "\b"); + escapes.put("\\f", "\f"); + escapes.put("\\n", "\n"); + escapes.put("\\r", "\r"); + escapes.put("\\t", "\t"); + escapes.put("\\0", "\0"); + escapes.put("\\/", "/"); + for(String c : escapes.keySet()) { + AbstractElement e = assertDoesNotThrow(() -> new JsonParser().parse("\"" + c + "\"")); + assertTrue(e.isString()); + assertEquals(escapes.get(c), e.string()); + } + } + + @Test + public void testParseStringUnicode() { + AbstractElement e = assertDoesNotThrow(() -> new JsonParser().parse("\"\\u001F\"")); + assertTrue(e.isString()); + assertEquals("\u001F", e.string()); + } + + @Test + public void testParseValidObject() { + AbstractElement e = assertDoesNotThrow(() -> new JsonParser().parse("{\"a\":1,\"b\":2}")); + assertTrue(e.isObject()); + assertEquals(2, e.object().size()); + List keys = new ArrayList<>(e.object().keys()); + assertEquals("a", keys.get(0)); + assertEquals("b", keys.get(1)); + AbstractElement aElement = e.object().get("a"); + assertTrue(aElement.isNumber()); + assertEquals(1, aElement.number()); + AbstractElement bElement = e.object().get("b"); + assertTrue(bElement.isNumber()); + assertEquals(2, bElement.number()); + } + + @Test + public void testParseInvalidObjectWithMissingColon() { + assertThrows(ParseException.class, () -> new JsonParser().parse("{\"a\"1}")); + } + + @Test + public void testParseInvalidObjectWithMissingComma() { + assertThrows(ParseException.class, () -> new JsonParser().parse("{\"a\":1\"b\":2}")); + } + + @Test + public void testParseValidArray() { + AbstractElement e = assertDoesNotThrow(() -> new JsonParser().parse("[1,2]")); + assertTrue(e.isArray()); + assertEquals(2, e.array().size()); + AbstractElement firstElement = e.array().get(0); + assertTrue(firstElement.isNumber()); + assertEquals(1, firstElement.number()); + AbstractElement secondElement = e.array().get(1); + assertTrue(secondElement.isNumber()); + assertEquals(2, secondElement.number()); + } + + @Test + public void testParseInvalidArrayWithInvalidValue() { + assertThrows(ParseException.class, () -> new JsonParser().parse("[talse]")); + } + + @Test + public void testParseInvalidArrayWithMissingComma() { + assertThrows(ParseException.class, () -> new JsonParser().parse("[\"a\"\"b\"]")); + } + +} From 753384eefa43e93664e2eb6476a0ff981ce35c47 Mon Sep 17 00:00:00 2001 From: JanHolger Date: Sun, 10 Mar 2024 16:21:38 +0100 Subject: [PATCH 12/19] Refactored toYaml / fromYaml into separate classes to better separate concerns and improve testability for a future custom implementation --- .../abstractdata/AbstractElement.java | 30 ++++++------------- .../abstractdata/yaml/LegacyYamlDumper.java | 22 ++++++++++++++ .../abstractdata/yaml/LegacyYamlParser.java | 25 ++++++++++++++++ .../abstractdata/yaml/YamlDumper.java | 22 ++++++++++++++ .../abstractdata/yaml/YamlParser.java | 22 ++++++++++++++ 5 files changed, 100 insertions(+), 21 deletions(-) create mode 100644 src/main/java/org/javawebstack/abstractdata/yaml/LegacyYamlDumper.java create mode 100644 src/main/java/org/javawebstack/abstractdata/yaml/LegacyYamlParser.java create mode 100644 src/main/java/org/javawebstack/abstractdata/yaml/YamlDumper.java create mode 100644 src/main/java/org/javawebstack/abstractdata/yaml/YamlParser.java diff --git a/src/main/java/org/javawebstack/abstractdata/AbstractElement.java b/src/main/java/org/javawebstack/abstractdata/AbstractElement.java index 852a783..a2a441d 100644 --- a/src/main/java/org/javawebstack/abstractdata/AbstractElement.java +++ b/src/main/java/org/javawebstack/abstractdata/AbstractElement.java @@ -7,6 +7,8 @@ import org.javawebstack.abstractdata.json.JsonDumper; import org.javawebstack.abstractdata.json.JsonParser; import org.javawebstack.abstractdata.util.QueryString; +import org.javawebstack.abstractdata.yaml.YamlDumper; +import org.javawebstack.abstractdata.yaml.YamlParser; import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.Yaml; @@ -110,16 +112,7 @@ default String toJsonString() { Object toObject(); default String toYaml(boolean pretty) { - Yaml yaml; - if (pretty) { - DumperOptions options = new DumperOptions(); - options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); - options.setPrettyFlow(true); - yaml = new Yaml(options); - } else { - yaml = new Yaml(); - } - return yaml.dump(toObject()); + return new YamlDumper().setPretty(pretty).dump(this); } default String toYaml() { @@ -127,24 +120,19 @@ default String toYaml() { } static AbstractElement fromYaml(String source, boolean singleRoot) { - Yaml yaml = new Yaml(); - Object object = yaml.load(source); - if (singleRoot && object instanceof List) { - List list = (List) object; - if (list.size() == 0) { - object = new HashMap<>(); - } else { - object = list.get(0); - } - } - return fromAbstractObject(object); + return new YamlParser().setSingleRoot(singleRoot).parse(source); } static AbstractElement fromYaml(String source) { return fromYaml(source, false); } + @Deprecated static AbstractElement fromAbstractObject(Object object) { + return fromObject(object); + } + + static AbstractElement fromObject(Object object) { if (object == null) return AbstractNull.VALUE; if (object instanceof List) { diff --git a/src/main/java/org/javawebstack/abstractdata/yaml/LegacyYamlDumper.java b/src/main/java/org/javawebstack/abstractdata/yaml/LegacyYamlDumper.java new file mode 100644 index 0000000..4dd7f37 --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/yaml/LegacyYamlDumper.java @@ -0,0 +1,22 @@ +package org.javawebstack.abstractdata.yaml; + +import org.javawebstack.abstractdata.AbstractElement; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; + +public class LegacyYamlDumper { + + protected static String dump(AbstractElement e, boolean pretty) { + Yaml yaml; + if (pretty) { + DumperOptions options = new DumperOptions(); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + options.setPrettyFlow(true); + yaml = new Yaml(options); + } else { + yaml = new Yaml(); + } + return yaml.dump(e.toObject()); + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/yaml/LegacyYamlParser.java b/src/main/java/org/javawebstack/abstractdata/yaml/LegacyYamlParser.java new file mode 100644 index 0000000..24399ee --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/yaml/LegacyYamlParser.java @@ -0,0 +1,25 @@ +package org.javawebstack.abstractdata.yaml; + +import org.javawebstack.abstractdata.AbstractElement; +import org.yaml.snakeyaml.Yaml; + +import java.util.HashMap; +import java.util.List; + +public class LegacyYamlParser { + + protected static AbstractElement parse(String source, boolean singleRoot) { + Yaml yaml = new Yaml(); + Object object = yaml.load(source); + if (singleRoot && object instanceof List) { + List list = (List) object; + if (list.size() == 0) { + object = new HashMap<>(); + } else { + object = list.get(0); + } + } + return AbstractElement.fromObject(object); + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/yaml/YamlDumper.java b/src/main/java/org/javawebstack/abstractdata/yaml/YamlDumper.java new file mode 100644 index 0000000..f92734b --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/yaml/YamlDumper.java @@ -0,0 +1,22 @@ +package org.javawebstack.abstractdata.yaml; + +import org.javawebstack.abstractdata.AbstractElement; + +public class YamlDumper { + + private boolean pretty = false; + + public YamlDumper setPretty(boolean pretty) { + this.pretty = pretty; + return this; + } + + public boolean isPretty() { + return pretty; + } + + public String dump(AbstractElement e) { + return LegacyYamlDumper.dump(e, pretty); + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/yaml/YamlParser.java b/src/main/java/org/javawebstack/abstractdata/yaml/YamlParser.java new file mode 100644 index 0000000..7eacc5a --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/yaml/YamlParser.java @@ -0,0 +1,22 @@ +package org.javawebstack.abstractdata.yaml; + +import org.javawebstack.abstractdata.AbstractElement; + +public class YamlParser { + + boolean singleRoot = false; + + public YamlParser setSingleRoot(boolean singleRoot) { + this.singleRoot = singleRoot; + return this; + } + + public boolean isSingleRoot() { + return singleRoot; + } + + public AbstractElement parse(String source) { + return LegacyYamlParser.parse(source, singleRoot); + } + +} From 44cde78f7dc7f005016ab4829e3b9d4bb4d30f5e Mon Sep 17 00:00:00 2001 From: JanHolger Date: Sun, 10 Mar 2024 17:16:26 +0100 Subject: [PATCH 13/19] Implemented tests for YamlParser and YamlDumper, bumped bson to 5.0.0 --- pom.xml | 2 +- .../abstractdata/AbstractElement.java | 6 +- .../abstractdata/yaml/LegacyYamlDumper.java | 6 +- .../abstractdata/yaml/LegacyYamlParser.java | 29 ++++++--- .../abstractdata/yaml/YamlParser.java | 4 +- .../abstractdata/yaml/YamlDumperTest.java | 38 ++++++++++++ .../abstractdata/yaml/YamlParserTest.java | 62 +++++++++++++++++++ 7 files changed, 131 insertions(+), 16 deletions(-) create mode 100644 src/test/java/org/javawebstack/abstractdata/yaml/YamlDumperTest.java create mode 100644 src/test/java/org/javawebstack/abstractdata/yaml/YamlParserTest.java diff --git a/pom.xml b/pom.xml index d012168..901fc6b 100644 --- a/pom.xml +++ b/pom.xml @@ -49,7 +49,7 @@ org.mongodb bson - 4.11.1 + 5.0.0 true diff --git a/src/main/java/org/javawebstack/abstractdata/AbstractElement.java b/src/main/java/org/javawebstack/abstractdata/AbstractElement.java index a2a441d..2db30ca 100644 --- a/src/main/java/org/javawebstack/abstractdata/AbstractElement.java +++ b/src/main/java/org/javawebstack/abstractdata/AbstractElement.java @@ -120,7 +120,11 @@ default String toYaml() { } static AbstractElement fromYaml(String source, boolean singleRoot) { - return new YamlParser().setSingleRoot(singleRoot).parse(source); + try { + return new YamlParser().setSingleRoot(singleRoot).parse(source); + } catch (ParseException e) { + throw new RuntimeException(e); + } } static AbstractElement fromYaml(String source) { diff --git a/src/main/java/org/javawebstack/abstractdata/yaml/LegacyYamlDumper.java b/src/main/java/org/javawebstack/abstractdata/yaml/LegacyYamlDumper.java index 4dd7f37..937540b 100644 --- a/src/main/java/org/javawebstack/abstractdata/yaml/LegacyYamlDumper.java +++ b/src/main/java/org/javawebstack/abstractdata/yaml/LegacyYamlDumper.java @@ -8,14 +8,14 @@ public class LegacyYamlDumper { protected static String dump(AbstractElement e, boolean pretty) { Yaml yaml; + DumperOptions options = new DumperOptions(); if (pretty) { - DumperOptions options = new DumperOptions(); options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); options.setPrettyFlow(true); - yaml = new Yaml(options); } else { - yaml = new Yaml(); + options.setPrettyFlow(false); } + yaml = new Yaml(options); return yaml.dump(e.toObject()); } diff --git a/src/main/java/org/javawebstack/abstractdata/yaml/LegacyYamlParser.java b/src/main/java/org/javawebstack/abstractdata/yaml/LegacyYamlParser.java index 24399ee..fe230f5 100644 --- a/src/main/java/org/javawebstack/abstractdata/yaml/LegacyYamlParser.java +++ b/src/main/java/org/javawebstack/abstractdata/yaml/LegacyYamlParser.java @@ -1,25 +1,34 @@ package org.javawebstack.abstractdata.yaml; import org.javawebstack.abstractdata.AbstractElement; +import org.javawebstack.abstractdata.AbstractNull; import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.parser.ParserException; -import java.util.HashMap; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; public class LegacyYamlParser { - protected static AbstractElement parse(String source, boolean singleRoot) { - Yaml yaml = new Yaml(); - Object object = yaml.load(source); - if (singleRoot && object instanceof List) { - List list = (List) object; - if (list.size() == 0) { - object = new HashMap<>(); - } else { + protected static AbstractElement parse(String source, boolean singleRoot) throws ParseException { + if(source.trim().equalsIgnoreCase("null")) + return AbstractNull.VALUE; + if(source.trim().isEmpty()) + throw new ParseException("Invalid yaml", 0); + try { + Yaml yaml = new Yaml(); + List list = new ArrayList<>(); + yaml.loadAll(source).forEach(list::add); + Object object = list; + if (singleRoot && list.size() == 1) { object = list.get(0); } + return AbstractElement.fromObject(object); + } catch (ParserException e) { + throw new ParseException(e.getMessage(), e.getProblemMark().getIndex()); } - return AbstractElement.fromObject(object); } } diff --git a/src/main/java/org/javawebstack/abstractdata/yaml/YamlParser.java b/src/main/java/org/javawebstack/abstractdata/yaml/YamlParser.java index 7eacc5a..14b8177 100644 --- a/src/main/java/org/javawebstack/abstractdata/yaml/YamlParser.java +++ b/src/main/java/org/javawebstack/abstractdata/yaml/YamlParser.java @@ -2,6 +2,8 @@ import org.javawebstack.abstractdata.AbstractElement; +import java.text.ParseException; + public class YamlParser { boolean singleRoot = false; @@ -15,7 +17,7 @@ public boolean isSingleRoot() { return singleRoot; } - public AbstractElement parse(String source) { + public AbstractElement parse(String source) throws ParseException { return LegacyYamlParser.parse(source, singleRoot); } diff --git a/src/test/java/org/javawebstack/abstractdata/yaml/YamlDumperTest.java b/src/test/java/org/javawebstack/abstractdata/yaml/YamlDumperTest.java new file mode 100644 index 0000000..d211544 --- /dev/null +++ b/src/test/java/org/javawebstack/abstractdata/yaml/YamlDumperTest.java @@ -0,0 +1,38 @@ +package org.javawebstack.abstractdata.yaml; + +import org.javawebstack.abstractdata.AbstractArray; +import org.javawebstack.abstractdata.AbstractObject; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +public class YamlDumperTest { + + @Test + public void testDump() { + AbstractObject object = new AbstractObject() + .set("a", 1) + .set("b", new AbstractArray()); + String expected = "a: 1\nb: []\n"; // Not actually minified but that's what snakeyaml outputs + String dumped = new YamlDumper().dump(object); + assertEquals(expected, dumped); + } + + @Test + public void testDumpPretty() { + AbstractObject object = new AbstractObject() + .set("a", 1) + .set("b", new AbstractArray()); + String expected = "a: 1\nb: [\n ]\n"; // Not actually expected, as it's not pretty but that's what snakeyaml calls pretty + String dumped = new YamlDumper().setPretty(true).dump(object); + assertEquals(expected, dumped); + } + + @Test + public void testSetPretty() { + YamlDumper dumper = new YamlDumper(); + assertFalse(dumper.isPretty()); + dumper.setPretty(true); + assertTrue(dumper.isPretty()); + } + +} diff --git a/src/test/java/org/javawebstack/abstractdata/yaml/YamlParserTest.java b/src/test/java/org/javawebstack/abstractdata/yaml/YamlParserTest.java new file mode 100644 index 0000000..770264f --- /dev/null +++ b/src/test/java/org/javawebstack/abstractdata/yaml/YamlParserTest.java @@ -0,0 +1,62 @@ +package org.javawebstack.abstractdata.yaml; + +import org.javawebstack.abstractdata.AbstractElement; +import org.junit.jupiter.api.Test; +import org.yaml.snakeyaml.Yaml; + +import java.text.ParseException; + +import static org.junit.jupiter.api.Assertions.*; + +public class YamlParserTest { + + @Test + public void testParse() { + AbstractElement e = assertDoesNotThrow(() -> new YamlParser().setSingleRoot(true).parse("test:\n a: 123\n b: false\n c: 'abc'")); + assertNotNull(e); + assertTrue(e.isObject()); + assertEquals(1, e.object().size()); + AbstractElement testElement = e.object().get("test"); + assertNotNull(testElement); + assertTrue(testElement.isObject()); + assertEquals(3, testElement.object().size()); + AbstractElement aElement = testElement.object().get("a"); + assertNotNull(aElement); + assertTrue(aElement.isNumber()); + assertEquals(123, aElement.number()); + AbstractElement bElement = testElement.object().get("b"); + assertNotNull(bElement); + assertTrue(bElement.isBoolean()); + assertEquals(false, bElement.bool()); + AbstractElement cElement = testElement.object().get("c"); + assertNotNull(cElement); + assertTrue(cElement.isString()); + assertEquals("abc", cElement.string()); + } + + @Test + public void testParseEmptyDocument() { + assertThrows(ParseException.class, () -> new YamlParser().parse("")); + } + + @Test + public void testParseNull() { + AbstractElement e = assertDoesNotThrow(() -> new YamlParser().parse("null")); + assertNotNull(e); + assertTrue(e.isNull()); + } + + @Test + public void testParseInvalidDocument() { + assertThrows(ParseException.class, () -> new YamlParser().parse("\"a\"1")); + } + + @Test + public void testSetSingleRoot() { + YamlParser parser = new YamlParser(); + assertFalse(parser.isSingleRoot()); + parser.setSingleRoot(true); + assertTrue(parser.isSingleRoot()); + } + +} From 13eb1c0cc0193ee459751084a7439deba06e0600 Mon Sep 17 00:00:00 2001 From: JanHolger Date: Wed, 4 Feb 2026 14:46:47 +0100 Subject: [PATCH 14/19] Switched from OSSRH to new central --- pom.xml | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/pom.xml b/pom.xml index 901fc6b..3fd01ca 100644 --- a/pom.xml +++ b/pom.xml @@ -68,19 +68,17 @@ - - maven-deploy-plugin - 2.7 - - - default-deploy - deploy - - deploy - - - - + + + org.sonatype.central + central-publishing-maven-plugin + 0.9.0 + true + + central + + + org.apache.maven.plugins maven-source-plugin @@ -119,7 +117,7 @@ sign - 6802C125C9069802B97A080910CE6063CEE9C47B + EC9CCFF8901F0AA22191DCEDD619376246C066D0 gpg --no-tty @@ -135,12 +133,12 @@ - ossrh - https://s01.oss.sonatype.org/content/repositories/snapshots + central + https://central.sonatype.com - ossrh - https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ + central + https://central.sonatype.com From 20d9eada7067664e2712ec5ec7896a491513e96f Mon Sep 17 00:00:00 2001 From: JanHolger Date: Wed, 4 Feb 2026 14:47:45 +0100 Subject: [PATCH 15/19] Switched from OSSRH to new central --- pom.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 3fd01ca..4d483b0 100644 --- a/pom.xml +++ b/pom.xml @@ -68,8 +68,7 @@ - - + org.sonatype.central central-publishing-maven-plugin 0.9.0 From 2f7a5796a0e3dbea1f3641e7320eb74149af5262 Mon Sep 17 00:00:00 2001 From: JanHolger Date: Wed, 4 Feb 2026 14:48:29 +0100 Subject: [PATCH 16/19] Switched from OSSRH to new central --- pom.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 4d483b0..98244ff 100644 --- a/pom.xml +++ b/pom.xml @@ -76,8 +76,7 @@ central - - + org.apache.maven.plugins maven-source-plugin From ca72359eb81fc2ead1f9a6f4b5d3da90c9469251 Mon Sep 17 00:00:00 2001 From: JanHolger Date: Wed, 4 Feb 2026 15:07:16 +0100 Subject: [PATCH 17/19] Switched from OSSRH to new central --- .github/workflows/maven-deploy-release.yml | 7 +++---- .github/workflows/maven-deploy.yml | 7 +++---- build/settings.xml | 16 +++------------- pom.xml | 2 +- 4 files changed, 10 insertions(+), 22 deletions(-) diff --git a/.github/workflows/maven-deploy-release.yml b/.github/workflows/maven-deploy-release.yml index 2dd9cd8..49b71b5 100644 --- a/.github/workflows/maven-deploy-release.yml +++ b/.github/workflows/maven-deploy-release.yml @@ -20,7 +20,6 @@ jobs: - name: Deploy to OSSRH Repository run: mvn deploy -DbuildVersion=${{ github.event.release.tag_name }} -s build/settings.xml env: - DEPLOYMENT_USERNAME: ${{ secrets.DEPLOYMENT_USERNAME }} - DEPLOYMENT_PASSWORD: ${{ secrets.DEPLOYMENT_PASSWORD }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} \ No newline at end of file + CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }} + CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} \ No newline at end of file diff --git a/.github/workflows/maven-deploy.yml b/.github/workflows/maven-deploy.yml index 1141b8d..4554782 100644 --- a/.github/workflows/maven-deploy.yml +++ b/.github/workflows/maven-deploy.yml @@ -21,7 +21,6 @@ jobs: - name: Deploy to OSSRH Repository run: mvn deploy -s build/settings.xml env: - DEPLOYMENT_USERNAME: ${{ secrets.DEPLOYMENT_USERNAME }} - DEPLOYMENT_PASSWORD: ${{ secrets.DEPLOYMENT_PASSWORD }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} \ No newline at end of file + CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }} + CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} \ No newline at end of file diff --git a/build/settings.xml b/build/settings.xml index 07e7726..88571fa 100644 --- a/build/settings.xml +++ b/build/settings.xml @@ -5,19 +5,9 @@ > - javawebstack-snapshots - ${env.DEPLOYMENT_USERNAME} - ${env.DEPLOYMENT_PASSWORD} - - - javawebstack-releases - ${env.DEPLOYMENT_USERNAME} - ${env.DEPLOYMENT_PASSWORD} - - - ossrh - JavaWebStack - ${env.OSSRH_PASSWORD} + central + ${env.CENTRAL_USERNAME} + ${env.CENTRAL_PASSWORD} gpg diff --git a/pom.xml b/pom.xml index 98244ff..2af6d5a 100644 --- a/pom.xml +++ b/pom.xml @@ -106,7 +106,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.0.1 + 3.2.4 sign-artifacts From 3df9eccdb80d3c703eb1c635d59afd8fd9a75d75 Mon Sep 17 00:00:00 2001 From: JanHolger Date: Wed, 4 Feb 2026 15:12:37 +0100 Subject: [PATCH 18/19] Switched from OSSRH to new central --- pom.xml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 2af6d5a..b2c36fd 100644 --- a/pom.xml +++ b/pom.xml @@ -132,12 +132,8 @@ central - https://central.sonatype.com + https://central.sonatype.com/repository/maven-snapshots/ - - central - https://central.sonatype.com - \ No newline at end of file From 10269e6d58a4514c99e3212bc3490a02ed0a6424 Mon Sep 17 00:00:00 2001 From: Julian Gojani Date: Sat, 30 May 2026 22:30:09 +0200 Subject: [PATCH 19/19] Update pom.xml --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b2c36fd..5353647 100644 --- a/pom.xml +++ b/pom.xml @@ -136,4 +136,4 @@ - \ No newline at end of file +