From 435cfa710e3d4b114cb2ef71a483a945c098f4f1 Mon Sep 17 00:00:00 2001 From: singingbush Date: Mon, 8 Jan 2024 23:51:50 +0000 Subject: [PATCH] WIP: serialise & deserialise annotated pojos --- annotations/pom.xml | 3 + .../sdl/annotations/Attribute.java | 2 +- .../singingbush/sdl/annotations/Value.java | 2 +- sdlang/pom.xml | 9 ++ .../com/singingbush/sdl/LineCommentStyle.java | 20 +++ .../main/java/com/singingbush/sdl/Parser.java | 106 ++++++++++++- .../main/java/com/singingbush/sdl/SDL.java | 120 ++++++++++++++- .../sdl/SdlAnnotationProcessor.java | 144 ++++++++++++++++++ .../main/java/com/singingbush/sdl/Tag.java | 135 ++++++++-------- .../java/com/singingbush/sdl/TagBuilder.java | 22 ++- .../main/java/com/singingbush/sdl/Token.java | 4 +- .../sdl/annotations/AnnotationTest.java | 105 +++++++++++++ 12 files changed, 590 insertions(+), 82 deletions(-) create mode 100644 sdlang/src/main/java/com/singingbush/sdl/LineCommentStyle.java create mode 100644 sdlang/src/main/java/com/singingbush/sdl/SdlAnnotationProcessor.java create mode 100644 sdlang/src/test/java/com/singingbush/sdl/annotations/AnnotationTest.java diff --git a/annotations/pom.xml b/annotations/pom.xml index a439bb8..705fa62 100644 --- a/annotations/pom.xml +++ b/annotations/pom.xml @@ -11,6 +11,9 @@ sdlang-annotations + SDLang Annotations + Support for SDLang (Simple Declarative Language) + org.jetbrains diff --git a/annotations/src/main/java/com/singingbush/sdl/annotations/Attribute.java b/annotations/src/main/java/com/singingbush/sdl/annotations/Attribute.java index 51a8d87..58f3416 100644 --- a/annotations/src/main/java/com/singingbush/sdl/annotations/Attribute.java +++ b/annotations/src/main/java/com/singingbush/sdl/annotations/Attribute.java @@ -5,7 +5,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER }) +@Target({ ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) public @interface Attribute { diff --git a/annotations/src/main/java/com/singingbush/sdl/annotations/Value.java b/annotations/src/main/java/com/singingbush/sdl/annotations/Value.java index 027286f..a1ad5ee 100644 --- a/annotations/src/main/java/com/singingbush/sdl/annotations/Value.java +++ b/annotations/src/main/java/com/singingbush/sdl/annotations/Value.java @@ -5,7 +5,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER }) +@Target({ ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) public @interface Value { } diff --git a/sdlang/pom.xml b/sdlang/pom.xml index 7970bdc..86d3e12 100644 --- a/sdlang/pom.xml +++ b/sdlang/pom.xml @@ -11,7 +11,16 @@ sdlang + SDLang + Support for SDLang (Simple Declarative Language) + + + com.singingbush + sdlang-annotations + ${project.version} + + org.jetbrains annotations diff --git a/sdlang/src/main/java/com/singingbush/sdl/LineCommentStyle.java b/sdlang/src/main/java/com/singingbush/sdl/LineCommentStyle.java new file mode 100644 index 0000000..2083181 --- /dev/null +++ b/sdlang/src/main/java/com/singingbush/sdl/LineCommentStyle.java @@ -0,0 +1,20 @@ +package com.singingbush.sdl; + +/** + * SDLang supports three styles of line comments + */ +public enum LineCommentStyle { + CPP("//"), + SHELL("#"), + LUA("--"); + + private final String value; + + LineCommentStyle(final String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/sdlang/src/main/java/com/singingbush/sdl/Parser.java b/sdlang/src/main/java/com/singingbush/sdl/Parser.java index cb99b35..937711e 100644 --- a/sdlang/src/main/java/com/singingbush/sdl/Parser.java +++ b/sdlang/src/main/java/com/singingbush/sdl/Parser.java @@ -16,15 +16,20 @@ */ package com.singingbush.sdl; +import com.singingbush.sdl.annotations.Attribute; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.*; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Type; import java.math.BigDecimal; import java.time.*; import java.time.format.DateTimeFormatter; import java.util.*; +import java.util.stream.Collectors; import static java.nio.charset.StandardCharsets.UTF_8; @@ -78,11 +83,110 @@ public Parser(@NotNull final File file) throws FileNotFoundException { this(new InputStreamReader(new FileInputStream(file), UTF_8)); } + /** + * For use with pojo's that have been annotated with the Tag annotation + * @param clazz a class that's annotated with Tag + * @param the class type that will be returned + * @return a pojo of the given type + * @throws IOException If a problem is encountered with the reader + * @throws SDLParseException If the document is malformed + * @since 2.3.0 + * @see com.singingbush.sdl.annotations.Tag + */ + public Optional parse(final Class clazz) throws IOException, SDLParseException { + if(!clazz.isAnnotationPresent(com.singingbush.sdl.annotations.Tag.class)) { + throw new IllegalArgumentException("class must be annotated with @Tag"); + } + + final String name = clazz.getAnnotation(com.singingbush.sdl.annotations.Tag.class).value(); + final List tags = this.parse().stream() + .filter(t -> name.equals(t.getName())) + .collect(Collectors.toList()); + + if(tags.isEmpty()) { + return Optional.empty(); + } + + if(tags.size() > 1) { + // need to call alternative method that can handle List + throw new IllegalArgumentException(String.format("Multiple tags found named %s", name)); + } + + final T result; + try { + result = clazz.getDeclaredConstructor().newInstance(); + // result = clazz.getDeclaredConstructor(String.class, String.class, LocalDate.class).newInstance(); // todo: support constructor injection + } catch (InstantiationException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + + final List values = tags.get(0).getValues(); + final SortedMap> attributes = tags.get(0).getAttributes(); + + for (final Field field : result.getClass().getDeclaredFields()) { + if(field.isAnnotationPresent(com.singingbush.sdl.annotations.Value.class)) { + try { + field.setAccessible(true); // or field.trySetAccessible(); + field.set(result, values.get(0)); // todo: how does this work with multiple values, also handle errors + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + if(field.isAnnotationPresent(com.singingbush.sdl.annotations.Attribute.class)) { + final SdlValue sdlValue = attributes.get(field.getName()); // also handle by @Attribute value + try { + field.setAccessible(true); + field.set(result, sdlValue.getValue()); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + //final Attribute[] attributeAnnotations = result.getClass().getDeclaredAnnotationsByType(Attribute.class); +// + //for(final Attribute attr : attributeAnnotations) { + // final SdlValue sdlValue = attributes.get(attr.value()); + // + // + //} + + //tags.get(0).getValues(); + + return Optional.of(result); + } + + /* + * For use with pojo's that have been annotated with the Tag annotation + * @param typeOfT a type that's annotated with Tag + * @param the class type that will be returned + * @return + * @throws IOException If a problem is encountered with the reader + * @throws SDLParseException If the document is malformed + * @since 2.3.0 + * @see com.singingbush.sdl.annotations.Tag + * + public Optional parse(final Type typeOfT) throws IOException, SDLParseException { + if(!typeOfT.getClass().isAnnotationPresent(com.singingbush.sdl.annotations.Tag.class)) { + throw new IllegalArgumentException("class must be annotated with @Tag"); + } + + return parse(typeOfT.getClass()); // todo + } + */ + /** * @return A list of tags described by the input * @throws IOException If a problem is encountered with the reader * @throws SDLParseException If the document is malformed */ + @NotNull public List parse() throws IOException, SDLParseException { final List tags = new ArrayList<>(); List toks; @@ -182,7 +286,7 @@ Tag constructTag(List toks) throws SDLParseException { valuesStartIndex = 3; } else { - tag = new Tag(t0.getText()); + tag = SDL.tag(t0.getText()).build(); } // read values diff --git a/sdlang/src/main/java/com/singingbush/sdl/SDL.java b/sdlang/src/main/java/com/singingbush/sdl/SDL.java index d39b15b..91186fb 100644 --- a/sdlang/src/main/java/com/singingbush/sdl/SDL.java +++ b/sdlang/src/main/java/com/singingbush/sdl/SDL.java @@ -19,12 +19,15 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.io.*; +import java.lang.reflect.Type; import java.math.BigDecimal; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.time.*; import java.time.format.DateTimeFormatter; import java.util.List; +import java.util.Optional; import java.util.SortedMap; import java.util.Base64; @@ -35,7 +38,7 @@ */ public class SDL { - public static final SdlValue NULL = new SdlValue<>(null, SdlType.NULL); + public static final SdlValue NULL = new SdlValue<>(null, SdlType.NULL); /** *

The SDL standard date format "yyyy/MM/dd" or "y/M/d"

@@ -222,7 +225,7 @@ private static String escape(Character ch) { * type */ // @SuppressWarnings("unchecked") -// public static SdlValue coerceOrFail(final SdlValue value) { +// public static SdlValue coerceOrFail(final SdlValue value) { // if(value == null) // return null; // @@ -346,6 +349,52 @@ public static SdlValue value(@NotNull final String value, final boolean new SdlValue<>(Parser.parseString(String.format("\"%s\"", value)), SdlType.STRING); } + /** + * + * @param value + * @param sdlType should be a literal type: STRING, STRING_MULTILINE, CHARACTER, BOOLEAN, NUMBER, DATE, DATETIME, DURATION, BINARY, NULL + * @return + * @since 2.3.0 + */ + // experimental (keep for internal use for now) + static SdlValue value(@NotNull final Object value, final SdlType sdlType) { + // STRING, STRING_MULTILINE, CHARACTER, BOOLEAN, NUMBER, DATE, DATETIME, DURATION, BINARY, NULL + switch (sdlType) { + case STRING: + return value(String.valueOf(value), false); + case STRING_MULTILINE: + return value(String.valueOf(value), true); + case CHARACTER: + return value((char) value); + case BOOLEAN: + return value((boolean) value); + case NUMBER: + // handle number types: int, long, float, double + if(Integer.class.isAssignableFrom(value.getClass())) { + return value((int) value); + } else if(Long.class.isAssignableFrom(value.getClass())) { + return value((long) value); + } else if(Float.class.isAssignableFrom(value.getClass())) { + return value((float) value); + } else if(Double.class.isAssignableFrom(value.getClass())) { + return value((double) value); + } else { + throw new IllegalArgumentException(String.format("SdlType was NUMBER but value of type %s could not be assigned", value.getClass())); + } + case DATE: + return value((LocalDate) value); + case DATETIME: + return value((LocalDateTime) value); + case DURATION: + return value((Duration) value); + // case BINARY: + // return value(); + // break; + default: + throw new IllegalArgumentException("Should be a literal type"); + } + } + /** * @param value text to be converted to SDL * @return an SDL char @@ -441,7 +490,7 @@ public static SdlValue value(@NotNull final Duration value) { * @return an SDL binary * @since 2.1.0 */ - public static SdlValue value(final byte[] value) { + public static SdlValue value(final byte[] value) { return new SdlValue<>(value, SdlType.BINARY); } @@ -455,7 +504,7 @@ public static SdlValue value(final byte[] value) { * @throws NumberFormatException If the text represents a malformed number. */ @Deprecated - public static SdlValue value(String literal) { + public static SdlValue value(String literal) { if(literal==null) { throw new IllegalArgumentException("literal argument to SDL.value(String) cannot be null"); } @@ -515,13 +564,16 @@ public static SdlValue value(String literal) { * @throws IllegalArgumentException If the string is null or contains * literals that cannot be parsed */ - public static List list(@NotNull final String valueList) { + public static List list(@NotNull final String valueList) { if(valueList==null) { throw new IllegalArgumentException("valueList argument to SDL.list(String) cannot be null"); } try { - return new Tag("root").read(valueList).getChild("content").getValues(); + return SDL.tag("root").build() + .read(valueList) + .getChild("content") + .getValues(); } catch(SDLParseException e) { throw new IllegalArgumentException(e.getMessage()); } @@ -555,13 +607,13 @@ public static List list(@NotNull final String valueList) { * @throws IllegalArgumentException If the string is null or contains * literals that cannot be parsed or the map is malformed */ - public static SortedMap map(@NotNull final String attributeString) { + public static SortedMap> map(@NotNull final String attributeString) { if(attributeString==null) { throw new IllegalArgumentException("attributeString argument to SDL.map(String) cannot be null"); } try { - return new Tag("root") + return SDL.tag("root").build() .read("atts " + attributeString) .getChild("atts") .getAttributes(); @@ -569,4 +621,56 @@ public static SortedMap map(@NotNull final String attributeStri throw new IllegalArgumentException(e.getMessage()); } } + + /** + * + * @param obj a pojo that's annotated with Tag + * @param out an output stream to write the serialised data to + * @throws IOException if an I/O error occurs + * @since 2.3.0 + * @see com.singingbush.sdl.annotations.Tag + */ + public static void toSDL(@NotNull Object obj, @NotNull final OutputStream out) throws IOException { + out.write(convert(obj).toString().getBytes()); + out.flush(); + } + + /** + * + * @param obj a pojo that's annotated with Tag + * @return a string of SDL that represents the annotated pojo + * @since 2.3.0 + * @see com.singingbush.sdl.annotations.Tag + */ + public static String toSDL(@NotNull Object obj) { + return convert(obj).toString(); + } + + + // todo: should this live somewhere else?? + private static Tag convert(@NotNull final Object obj) { + if(obj.getClass().isAnnotationPresent(com.singingbush.sdl.annotations.Tag.class)) { + final SdlAnnotationProcessor processor = new SdlAnnotationProcessor(obj); + return processor.process(); + } + throw new IllegalArgumentException(obj.getClass().getSimpleName() + " does not have @Tag annotation"); + } + + + public static Optional fromSDL(@NotNull final String sdl, @NotNull final Class clazz) throws SDLParseException, IOException { + return SDL.fromSDL(new StringReader(sdl), clazz); + } + +// public static Optional fromSDL(@NotNull final String sdl, @NotNull final Type typeOfT) throws SDLParseException { +// return SDL.fromSDL(new StringReader(sdl), typeOfT); +// } + + public static Optional fromSDL(@NotNull final Reader reader, @NotNull final Class clazz) throws SDLParseException, IOException { + return new Parser(reader).parse(clazz); + } + +// public static Optional fromSDL(@NotNull final Reader reader, @NotNull final Type typeOfT) { +// return new Parser(reader).parse(typeOfT); +// } + } diff --git a/sdlang/src/main/java/com/singingbush/sdl/SdlAnnotationProcessor.java b/sdlang/src/main/java/com/singingbush/sdl/SdlAnnotationProcessor.java new file mode 100644 index 0000000..eb1a586 --- /dev/null +++ b/sdlang/src/main/java/com/singingbush/sdl/SdlAnnotationProcessor.java @@ -0,0 +1,144 @@ +package com.singingbush.sdl; + +import com.singingbush.sdl.annotations.Attribute; +import com.singingbush.sdl.annotations.Value; + +import java.lang.reflect.Field; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.util.Arrays; + +/* +* Similar to com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector +* +*/ +public class SdlAnnotationProcessor { + + private final Object obj; + + public SdlAnnotationProcessor(Object obj) { + this.obj = obj; + if(!obj.getClass().isAnnotationPresent(com.singingbush.sdl.annotations.Tag.class)) { + throw new IllegalArgumentException(obj.getClass().getSimpleName() + " does not have @Tag annotation"); + } + } + + public Tag process() { + final com.singingbush.sdl.annotations.Tag tagAnn = obj.getClass().getAnnotation(com.singingbush.sdl.annotations.Tag.class); + + final TagBuilder tagBuilder = SDL.tag(tagAnn.value()).withNamespace(tagAnn.namespace()); + + Arrays.stream(obj.getClass().getDeclaredFields()) + .forEach(field -> { + + if(field.isAnnotationPresent(Value.class)) { + processValueField(tagBuilder, field); + } + + if(field.isAnnotationPresent(Attribute.class)) { + processAttributeField(tagBuilder, field); + } + }); + + + return tagBuilder.build(); + } + + private void processValueField(final TagBuilder tagBuilder, final Field field) { + field.setAccessible(true); + //final Value valAnn = field.getAnnotation(Value.class); + + try { + // SDL only supports a limited amount of literal types: + // STRING, STRING_MULTILINE, CHARACTER, BOOLEAN, NUMBER, DATE, DATETIME, DURATION, BINARY, NULL + // which are supported via the following Java types: + // String, Character, Boolean, Long, Float, Double, Integer, LocalDate, LocalDateTime, ZonedDateTime, Duration + // todo: handle objects that are also pojos annotated with @Tag + switch (field.getType().getSimpleName()) { + case "String": + tagBuilder.withValue(SDL.value(String.valueOf(field.get(obj)), false)); + break; + case "Character": + tagBuilder.withValue(SDL.value(Character.class.cast(field.get(obj)))); + break; + case "Boolean": + tagBuilder.withValue(SDL.value(Boolean.class.cast(field.get(obj)))); + break; + case "Long": + tagBuilder.withValue(SDL.value(Long.class.cast(field.get(obj)))); + break; + case "Float": + tagBuilder.withValue(SDL.value(Float.class.cast(field.get(obj)))); + break; + case "Double": + tagBuilder.withValue(SDL.value(Double.class.cast(field.get(obj)))); + break; + case "Integer": + tagBuilder.withValue(SDL.value(Integer.class.cast(field.get(obj)))); + break; + case "LocalDate": + tagBuilder.withValue(SDL.value(LocalDate.class.cast(field.get(obj)))); + break; + case "LocalDateTime": + tagBuilder.withValue(SDL.value(LocalDateTime.class.cast(field.get(obj)))); + break; + case "ZonedDateTime": + tagBuilder.withValue(SDL.value(ZonedDateTime.class.cast(field.get(obj)))); + break; + case "Duration": + tagBuilder.withValue(SDL.value(Duration.class.cast(field.get(obj)))); + break; + } + } catch (IllegalAccessException e) { + e.printStackTrace(); // todo: use slf4j-api to log error, also perhaps throw an Exception?? + } + } + + private void processAttributeField(final TagBuilder tagBuilder, final Field field) { + field.setAccessible(true); + final Attribute attrAnn = field.getAnnotation(Attribute.class); + final String name = !attrAnn.value().isEmpty() ? attrAnn.value() : field.getName(); + + try { + switch (field.getType().getSimpleName()) { + case "String": + tagBuilder.withAttribute(name, SDL.value(String.valueOf(field.get(obj)), false)); + break; + case "Character": + tagBuilder.withAttribute(name, SDL.value(Character.class.cast(field.get(obj)))); + break; + case "Boolean": + tagBuilder.withAttribute(name, SDL.value(Boolean.class.cast(field.get(obj)))); + break; + case "Long": + tagBuilder.withAttribute(name, SDL.value(Long.class.cast(field.get(obj)))); + break; + case "Float": + tagBuilder.withAttribute(name, SDL.value(Float.class.cast(field.get(obj)))); + break; + case "Double": + tagBuilder.withAttribute(name, SDL.value(Double.class.cast(field.get(obj)))); + break; + case "Integer": + tagBuilder.withAttribute(name, SDL.value(Integer.class.cast(field.get(obj)))); + break; + case "LocalDate": + tagBuilder.withAttribute(name, SDL.value(LocalDate.class.cast(field.get(obj)))); + break; + case "LocalDateTime": + tagBuilder.withAttribute(name, SDL.value(LocalDateTime.class.cast(field.get(obj)))); + break; + case "ZonedDateTime": + tagBuilder.withAttribute(name, SDL.value(ZonedDateTime.class.cast(field.get(obj)))); + break; + case "Duration": + tagBuilder.withAttribute(name, SDL.value(Duration.class.cast(field.get(obj)))); + break; + } + } catch (IllegalAccessException e) { + e.printStackTrace(); // todo: use slf4j-api to log error, also perhaps throw an Exception?? + } + } +} diff --git a/sdlang/src/main/java/com/singingbush/sdl/Tag.java b/sdlang/src/main/java/com/singingbush/sdl/Tag.java index 8050d82..aa334fb 100644 --- a/sdlang/src/main/java/com/singingbush/sdl/Tag.java +++ b/sdlang/src/main/java/com/singingbush/sdl/Tag.java @@ -20,8 +20,6 @@ import org.jetbrains.annotations.Nullable; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; @@ -30,6 +28,8 @@ import java.io.StringReader; import java.io.Writer; import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.*; import java.util.Map.Entry; import java.util.stream.Collectors; @@ -342,14 +342,11 @@ public class Tag implements Serializable { private final String name; private String comment; - private List values = new ArrayList(); - private List valuesView = Collections.unmodifiableList(values); + private LineCommentStyle commentStyle; + private List> values = new ArrayList<>(); private Map attributeToNamespace = new HashMap<>(); - //private Map attributeToNamespaceView = Collections.unmodifiableMap(attributeToNamespace); - private SortedMap attributes = new TreeMap<>(); - //private SortedMap attributesView = Collections.unmodifiableSortedMap(attributes); + private SortedMap> attributes = new TreeMap<>(); private List children = new ArrayList<>(); - private List childrenView = Collections.unmodifiableList(children); /** * Creates an empty tag. @@ -398,7 +395,7 @@ public Tag(@NotNull final String namespace, @NotNull final String name) { * * @param child The child to add */ - public void addChild(Tag child) { + public void addChild(final Tag child) { children.add(child); } @@ -408,7 +405,7 @@ public void addChild(Tag child) { * @param child The child to remove * @return true if the child exists and is removed */ - public boolean removeChild(Tag child) { + public boolean removeChild(final Tag child) { return children.remove(child); } @@ -419,7 +416,7 @@ public boolean removeChild(Tag child) { * @param value The value to be set. * @throws IllegalArgumentException if the value is not a legal SDL type */ - public void setValue(SdlValue value) { + public void setValue(SdlValue value) { if(values.isEmpty()) { addValue(value); } else { @@ -434,7 +431,7 @@ public void setValue(SdlValue value) { * @since 2.0.0 */ @Nullable - public SdlValue getSdlValue() { + public SdlValue getSdlValue() { return values.isEmpty() ? null : values.get(0); } @@ -612,13 +609,13 @@ public List getChildrenForNamespace(String namespace, boolean recursive) { * @param name The name of the children from which values are retrieved * @return A list of values (or lists of values) */ - public List getChildrenValues(final String name) { - final ArrayList results = new ArrayList(); + public List getChildrenValues(final String name) { + final ArrayList results = new ArrayList<>(); final List children = getChildren(name); for(final Tag c : children) { - final List values = c.getValues(); + final List values = c.getValues(); if(values.isEmpty()) { results.add(null); } else if(values.size()==1) { @@ -638,7 +635,7 @@ public List getChildrenValues(final String name) { * * @param value The value to add */ - public void addValue(SdlValue value) { + public void addValue(final SdlValue value) { values.add(value); } @@ -648,7 +645,7 @@ public void addValue(SdlValue value) { * @param value The value to remove * @return true If the value exists and is removed */ - public boolean removeValue(SdlValue value) { + public boolean removeValue(final SdlValue value) { return values.remove(value); } @@ -658,7 +655,7 @@ public boolean removeValue(SdlValue value) { * @return An immutable view of the values. */ public List getValues() { - return valuesView.stream() + return values.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(values).stream() .filter(Objects::nonNull) .map(SdlValue::getValue) .collect(Collectors.toList()); @@ -691,7 +688,7 @@ public List getValues() { * identifier (see {@link SDL#validateIdentifier(String)}) or the * value is not a legal SDL type. */ - public void setAttribute(String key, SdlValue value) { + public void setAttribute(String key, SdlValue value) { setAttribute("", key, value); } @@ -708,12 +705,14 @@ public void setAttribute(String key, SdlValue value) { * namespace is non-blank and is not a legal SDL identifier, or the * value is not a legal SDL type */ - public void setAttribute(@Nullable String namespace, String key, SdlValue value) { - if(namespace==null) - namespace=""; + public void setAttribute(@Nullable String namespace, String key, SdlValue value) { + if(namespace==null) { + namespace=""; + } - if(namespace.length()!=0) - SDL.validateIdentifier(namespace); + if(!namespace.isEmpty()) { + SDL.validateIdentifier(namespace); + } SDL.validateIdentifier(key); attributeToNamespace.put(key, namespace); @@ -728,7 +727,7 @@ public void setAttribute(@Nullable String namespace, String key, SdlValue value) */ @Nullable public Object getAttribute(final String key) { - final SdlValue value = attributes.get(key); + final SdlValue value = attributes.get(key); return value != null ? value.getValue() : null; } @@ -747,7 +746,7 @@ public Object removeAttribute(String attributeKey) { * * @return An immutable view of the attributes. */ - public SortedMap getAttributes() { + public SortedMap> getAttributes() { return Collections.unmodifiableSortedMap(attributes); } @@ -760,13 +759,13 @@ public SortedMap getAttributes() { * identifier (see {@link SDL#validateIdentifier(String)}), or any value * is not a legal SDL type */ - public void setAttributes(Map attributes) { + public void setAttributes(final Map> attributes) { this.attributes.clear(); if(attributes!=null) { // this is required to ensure validation - for(Entry e : attributes.entrySet()) + for(Entry> e : attributes.entrySet()) setAttribute(e.getKey(), e.getValue()); } } @@ -810,7 +809,7 @@ public SortedMap getAttributesForNamespace(final String namespac * @return An immutable view of the children. */ public List getChildren() { - return childrenView; + return children.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(children); } /** @@ -821,15 +820,14 @@ public List getChildren() { * @return An immutable view of the children */ public List getChildren(boolean recursively) { - if(!recursively) - return childrenView; + if(!recursively) { + return getChildren(); + } - ArrayList kids = new ArrayList(); - for(Tag t:children) { + final ArrayList kids = new ArrayList<>(); + for(final Tag t : children) { kids.add(t); - - if(recursively) - kids.addAll(t.getChildren(true)); + kids.addAll(t.getChildren(true)); } return Collections.unmodifiableList(kids); @@ -857,8 +855,24 @@ public String getComment() { return comment; } + /** + * @param comment a single line of text that will precede the tag when serialised + * @since 2.1.0 + * @deprecated please use {@link Tag#setComment(String, LineCommentStyle)} + */ + @Deprecated public void setComment(final String comment) { + this.setComment(comment, LineCommentStyle.CPP); + } + + /** + * @param comment a single line of text that will precede the tag when serialised + * @param commentStyle either C++ style "//", bash style "#", or lua style "--" + * @since 2.3.0 + */ + public void setComment(final String comment, final LineCommentStyle commentStyle) { this.comment = comment; + this.commentStyle = commentStyle; } /** @@ -870,7 +884,7 @@ public void setComment(final String comment) { * @return This tag after adding all the children read from the reader */ public Tag read(final URL url) throws IOException, SDLParseException { - return read(new InputStreamReader(url.openStream(), "UTF8")); + return read(new InputStreamReader(url.openStream(), StandardCharsets.UTF_8)); } /** @@ -882,7 +896,7 @@ public Tag read(final URL url) throws IOException, SDLParseException { * @return This tag after adding all the children read from the reader */ public Tag read(final File file) throws IOException, SDLParseException { - return read(new InputStreamReader(new FileInputStream(file), "UTF8")); + return read(new InputStreamReader(Files.newInputStream(file.toPath()), StandardCharsets.UTF_8)); } /** @@ -936,8 +950,7 @@ public void write(File file) throws IOException { * @throws IOException If there is an IO problem during the write operation */ public void write(File file, boolean includeRoot) throws IOException { - write(new OutputStreamWriter(new FileOutputStream(file),"UTF8"), - includeRoot); + write(new OutputStreamWriter(Files.newOutputStream(file.toPath()),StandardCharsets.UTF_8), includeRoot); } /** @@ -948,16 +961,14 @@ public void write(File file, boolean includeRoot) throws IOException { * element, if false only the children will be written * @throws IOException If there is an IO problem during the write operation */ - public void write(Writer writer, boolean includeRoot) throws IOException { - String newLine = System.getProperty("line.separator"); - + public void write(final Writer writer, boolean includeRoot) throws IOException { if(includeRoot) { writer.write(toString()); } else { - for(Iterator i=children.iterator();i.hasNext();) { + for(final Iterator i = children.iterator(); i.hasNext();) { writer.write(String.valueOf(i.next())); if(i.hasNext()) - writer.write(newLine); + writer.write(System.lineSeparator()); } } @@ -983,7 +994,7 @@ public String toString() { * TODO: break up long lines using the backslash */ private String toString(@Nullable String linePrefix) { - String newLine = System.getProperty("line.separator"); + String newLine = System.lineSeparator(); if(linePrefix==null) linePrefix=""; @@ -993,7 +1004,7 @@ private String toString(@Nullable String linePrefix) { if(comment != null && !comment.isEmpty()) { final String[] lines = comment.split("\n"); for (final String line : lines) { - builder.append("// ").append(line).append(newLine).append(linePrefix); + builder.append(commentStyle.getValue()).append(" ").append(line).append(newLine).append(linePrefix); } } @@ -1008,7 +1019,7 @@ private String toString(@Nullable String linePrefix) { } // output values if(values != null && !values.isEmpty()) { - for(final SdlValue value : values) { + for(final SdlValue value : values) { if(skipValueSpace) { skipValueSpace=false; @@ -1030,28 +1041,27 @@ private String toString(@Nullable String linePrefix) { // output attributes if(attributes != null && !attributes.isEmpty()) { - for(Iterator> i = attributes.entrySet().iterator(); i.hasNext();) { - builder.append(" "); + for (Entry> e : attributes.entrySet()) { + builder.append(" "); - final Entry e = i.next(); - final String key=e.getKey(); + final String key = e.getKey(); final String attNamespace = attributeToNamespace.get(key); - if(attNamespace != null && !attNamespace.isEmpty()) { + if (attNamespace != null && !attNamespace.isEmpty()) { builder.append(attNamespace).append(":"); } - builder.append(key).append("="); - builder.append(attributes.get(key).getText()); - } + builder.append(key).append("="); + builder.append(attributes.get(key).getText()); + } } // output children if(children != null && !children.isEmpty()) { - builder.append(" {" + newLine); + builder.append(" {").append(newLine); for(final Tag t : children) { builder.append(t.toString(linePrefix + " ") + newLine); } - builder.append(linePrefix + "}"); + builder.append(linePrefix).append("}"); } return builder.toString(); @@ -1064,19 +1074,16 @@ public boolean equals(Object o) { final Tag tag = (Tag) o; return Objects.equals(namespace, tag.namespace) && Objects.equals(name, tag.name) && + // ignore comments and comment style Objects.equals(values, tag.values) && - Objects.equals(valuesView, tag.valuesView) && Objects.equals(attributeToNamespace, tag.attributeToNamespace) && - //Objects.equals(attributeToNamespaceView, tag.attributeToNamespaceView) && Objects.equals(attributes, tag.attributes) && - //Objects.equals(attributesView, tag.attributesView) && - Objects.equals(children, tag.children) && - Objects.equals(childrenView, tag.childrenView); + Objects.equals(children, tag.children); } @Override public int hashCode() { - return Objects.hash(namespace, name, values, valuesView, attributeToNamespace, attributes, children, childrenView); + return Objects.hash(namespace, name, values, attributeToNamespace, attributes, children); } } diff --git a/sdlang/src/main/java/com/singingbush/sdl/TagBuilder.java b/sdlang/src/main/java/com/singingbush/sdl/TagBuilder.java index cc6bc5d..0865631 100644 --- a/sdlang/src/main/java/com/singingbush/sdl/TagBuilder.java +++ b/sdlang/src/main/java/com/singingbush/sdl/TagBuilder.java @@ -18,9 +18,10 @@ public class TagBuilder { private final String name; private String namespace; private String comment; + private LineCommentStyle commentStyle = LineCommentStyle.CPP; private List values = new ArrayList<>(); private List children = new ArrayList<>(); - private Map attributes = new HashMap<>(); + private Map> attributes = new HashMap<>(); /** * @param name must be a legal SDL identifier (see {@link SDL#validateIdentifier(String)}) @@ -49,7 +50,18 @@ public TagBuilder withNamespace(@NotNull final String name) { * @since 2.1.0 */ public TagBuilder withComment(@NotNull final String comment) { + return this.withComment(comment, LineCommentStyle.CPP); + } + + /** + * @param comment a single line of text that will precede the tag when serialised + * @param commentStyle either C++ style "//", bash style "#", or lua style "--" + * @return this TagBuilder + * @since 2.3.0 + */ + public TagBuilder withComment(@NotNull final String comment, @NotNull final LineCommentStyle commentStyle) { this.comment = comment; + this.commentStyle = commentStyle; return this; } @@ -60,7 +72,7 @@ public TagBuilder withComment(@NotNull final String comment) { * @since 2.1.0 */ @NotNull - public TagBuilder withValue(@NotNull final SdlValue value) { + public TagBuilder withValue(@NotNull final SdlValue value) { this.values.add(value); return this; } @@ -133,7 +145,7 @@ public TagBuilder withChildren(@NotNull final List children) { * @since 2.1.0 */ @NotNull - public TagBuilder withAttribute(@NotNull final String key, @NotNull final SdlValue value) { + public TagBuilder withAttribute(@NotNull final String key, @NotNull final SdlValue value) { this.attributes.put(key, value); return this; } @@ -145,7 +157,7 @@ public TagBuilder withAttribute(@NotNull final String key, @NotNull final SdlVal * @since 2.1.0 */ @NotNull - public TagBuilder withAttributes(@NotNull final Map attributes) { + public TagBuilder withAttributes(@NotNull final Map> attributes) { this.attributes.putAll(attributes); return this; } @@ -159,7 +171,7 @@ public TagBuilder withAttributes(@NotNull final Map attributes @NotNull public Tag build() { final Tag t = namespace != null? new Tag(namespace, name) : new Tag(name); - t.setComment(comment); + t.setComment(comment, commentStyle); values.forEach(t::addValue); children.forEach(t::addChild); t.setAttributes(attributes); // attributes.forEach(t::setAttribute); diff --git a/sdlang/src/main/java/com/singingbush/sdl/Token.java b/sdlang/src/main/java/com/singingbush/sdl/Token.java index 43e276d..3a4ce65 100644 --- a/sdlang/src/main/java/com/singingbush/sdl/Token.java +++ b/sdlang/src/main/java/com/singingbush/sdl/Token.java @@ -22,7 +22,7 @@ class Token { private final int position; private final int size; - private SdlValue sdlValue; + private SdlValue sdlValue; private final boolean punctuation; private final boolean literal; @@ -144,7 +144,7 @@ public Object getObject() { * @return the SdlValue for this Token * @since 2.0.0 */ - public SdlValue getSdlValue() { + public SdlValue getSdlValue() { return sdlValue; } diff --git a/sdlang/src/test/java/com/singingbush/sdl/annotations/AnnotationTest.java b/sdlang/src/test/java/com/singingbush/sdl/annotations/AnnotationTest.java new file mode 100644 index 0000000..2052aec --- /dev/null +++ b/sdlang/src/test/java/com/singingbush/sdl/annotations/AnnotationTest.java @@ -0,0 +1,105 @@ +package com.singingbush.sdl.annotations; + +import com.singingbush.sdl.SDL; +import com.singingbush.sdl.SDLParseException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.time.LocalDate; +import java.util.Objects; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class AnnotationTest { + + @DisplayName("Ensure annotated pojo Serialization & Deserialization") + @Test + public void testAnnotatedPojoSerializationDeserialization() throws IOException, SDLParseException { + final Person p = new Person("bob", "bob@website.test", LocalDate.of(2000, 1, 28)); + + final String sdl = SDL.toSDL(p); + + assertEquals("person \"bob\" dob=2000/1/28 email=\"bob@website.test\"", sdl, "Annotated pojo should serialize correctly"); + + final Optional op = SDL.fromSDL(sdl, Person.class); + + assertTrue(op.isPresent()); + + final Person person = op.get(); + assertEquals("bob", person.getUsername()); + assertEquals("bob@website.test", person.getEmail()); + assertEquals(LocalDate.of(2000, 1, 28), person.getDob()); + + assertEquals(p, person); + } + + @DisplayName("Ensure deserialization ignores unrelated tags") + @Test + public void test() throws SDLParseException, IOException { + final Optional op = SDL.fromSDL( + "car \"Tesla\"\n" + + "person \"Dave\" dob=1995/5/23 email=\"dave@website.test\"\n" + + "house type=\"detached\"", + Person.class + ); + + assertTrue(op.isPresent()); + + final Person person = op.get(); + assertEquals("Dave", person.getUsername()); + assertEquals("dave@website.test", person.getEmail()); + assertEquals(LocalDate.of(1995, 5, 23), person.getDob()); + } + + /* + * This class is an example of how to annotate a pojo for serialization/derserialization to SDLang + */ + @Tag("person") + public static class Person { + + @Value + private String username; + + @Attribute + private String email; + + @Attribute("dob") + private LocalDate dob; + + public Person() {} // SDL annotations require nullary constructor at the minute + + public Person(String username, String email, LocalDate dob) { + this.username = username; + this.email = email; + this.dob = dob; + } + + public String getUsername() { + return username; + } + + public String getEmail() { + return email; + } + + public LocalDate getDob() { + return dob; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Person person = (Person) o; + return Objects.equals(username, person.username) && Objects.equals(email, person.email) && Objects.equals(dob, person.dob); + } + + @Override + public int hashCode() { + return Objects.hash(username, email, dob); + } + } +}