diff --git a/README.md b/README.md index 0c9cdc1..021e4ff 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.5 + 1.0.6 ``` diff --git a/pom.xml b/pom.xml index f7b062f..401f5c6 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ 8 8 - 1.0.5-SNAPSHOT + 1.0.6-SNAPSHOT org.javawebstack @@ -44,18 +44,18 @@ org.yaml snakeyaml - 1.33 + 2.2 org.mongodb bson - 4.7.1 + 4.10.2 true org.junit.jupiter junit-jupiter - 5.8.1 + 5.10.0 test diff --git a/src/main/java/org/javawebstack/abstractdata/AbstractArray.java b/src/main/java/org/javawebstack/abstractdata/AbstractArray.java index 20a613b..2d97ea4 100644 --- a/src/main/java/org/javawebstack/abstractdata/AbstractArray.java +++ b/src/main/java/org/javawebstack/abstractdata/AbstractArray.java @@ -352,6 +352,8 @@ public boolean equals(Object obj, boolean strict) { AbstractArray arr = ((AbstractElement) obj).array(strict); if (size() != arr.size()) return false; + if (size() == 0 || arr.size() == 0) + return false; for (int i = 0; i < size(); i++) { if (!(get(i).equals(arr.get(i), strict))) diff --git a/src/main/java/org/javawebstack/abstractdata/AbstractMapper.java b/src/main/java/org/javawebstack/abstractdata/AbstractMapper.java deleted file mode 100644 index 5069e8a..0000000 --- a/src/main/java/org/javawebstack/abstractdata/AbstractMapper.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.javawebstack.abstractdata; - -import org.javawebstack.abstractdata.mapper.Mapper; - -/** - * @deprecated will be removed in the next release, replaced by org.javawebstack.abstractdata.mapper.Mapper - */ -@Deprecated -public class AbstractMapper { - - private final Mapper mapper = new Mapper(); - private NamingPolicy namingPolicy = NamingPolicy.NONE; - private String dateFormat = "yyyy-MM-dd HH:mm:ss"; - - public AbstractMapper setNamingPolicy(NamingPolicy namingPolicy) { - this.namingPolicy = namingPolicy; - mapper.namingPolicy(namingPolicy.getMapperPolicy()); - return this; - } - - public boolean shouldOmitNull() { - return mapper.shouldOmitNull(); - } - - public AbstractMapper setOmitNull(boolean omitNull) { - this.mapper.omitNull(omitNull); - return this; - } - - public NamingPolicy getNamingPolicy() { - return namingPolicy; - } - - public AbstractMapper setExposeRequired(boolean exposeRequired) { - mapper.requireExpose(exposeRequired); - return this; - } - - public boolean isExposeRequired() { - return mapper.isExposeRequired(); - } - - public AbstractMapper setDateFormat(String dateFormat) { - this.dateFormat = dateFormat; - mapper.dateFormat(dateFormat); - return this; - } - - public String getDateFormat() { - return dateFormat; - } - - public AbstractElement toAbstract(Object object) { - return mapper.map(object); - } - - public T fromAbstract(AbstractElement element, Class type) { - if (element == null) - return null; - return mapper.map(element, type); - } - - -} diff --git a/src/main/java/org/javawebstack/abstractdata/mapper/DefaultMappers.java b/src/main/java/org/javawebstack/abstractdata/mapper/DefaultMappers.java index d8f49d5..2ed0b60 100644 --- a/src/main/java/org/javawebstack/abstractdata/mapper/DefaultMappers.java +++ b/src/main/java/org/javawebstack/abstractdata/mapper/DefaultMappers.java @@ -9,7 +9,6 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; -import java.sql.Time; import java.sql.Timestamp; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -28,7 +27,7 @@ public final class DefaultMappers { public static final FallbackMapper FALLBACK = new FallbackMapper(); public static Map, MapperTypeAdapter> create() { - Map, MapperTypeAdapter> map = new HashMap<>(); + Map, MapperTypeAdapter> map = new LinkedHashMap<>(); for (MapperTypeAdapter adapter : new MapperTypeAdapter[]{ ABSTRACT, @@ -64,20 +63,20 @@ public Object fromAbstract(MapperContext context, AbstractElement element, Class } if (type.equals(char.class) || type.equals(Character.class)) { if (type.isPrimitive() && element.isNull()) - throw new MapperWrongTypeException(context.getField().getName(), "number", "null"); + throw new MapperWrongTypeException(context.getFieldName(), "number", "null"); String s = element.string(context.getMapper().isStrict()); if (s.length() != 1) - throw new MapperException("Expected string of length 1 for field " + context.getField().getName() + " but received " + s.length()); + throw new MapperException("Expected string of length 1 for field " + context.getFieldName() + " but received " + s.length()); return s.charAt(0); } if (type.equals(Boolean.class) || type.equals(boolean.class)) { if (type.isPrimitive() && element.isNull()) - throw new MapperWrongTypeException(context.getField().getName(), "number", "null"); + throw new MapperWrongTypeException(context.getFieldName(), "number", "null"); return element.bool(context.getMapper().isStrict()); } if (Number.class.isAssignableFrom(type) || type.isPrimitive()) { if (type.isPrimitive() && element.isNull()) - throw new MapperWrongTypeException(context.getField().getName(), "number", "null"); + throw new MapperWrongTypeException(context.getFieldName(), "number", "null"); if (type.equals(int.class) || type.equals(Integer.class)) return element.number(context.getMapper().isStrict()).intValue(); if (type.equals(long.class) || type.equals(Long.class)) @@ -95,9 +94,9 @@ public Object fromAbstract(MapperContext context, AbstractElement element, Class return element.number(context.getMapper().isStrict()); } } catch (AbstractCoercingException ex) { - throw new MapperWrongTypeException(context.getField().getName(), "string", Helpers.typeName(element)); + throw new MapperWrongTypeException(context.getField() != null ? context.getFieldName() : "", "string", Helpers.typeName(element)); } - throw new MapperWrongTypeException(context.getField().getName(), "primitive", Helpers.typeName(element)); + throw new MapperWrongTypeException(context.getFieldName(), "primitive", Helpers.typeName(element)); } public Class[] getSupportedTypes() { @@ -136,7 +135,7 @@ public AbstractElement toAbstract(MapperContext context, Object value) throws Ma } public Object fromAbstract(MapperContext context, AbstractElement element, Class type) throws MapperException { - if (type.equals(List.class) || type.equals(AbstractList.class)) + if (type.equals(List.class) || type.equals(Collection.class) || type.equals(AbstractList.class)) type = ArrayList.class; if (type.equals(Set.class)) type = HashSet.class; @@ -157,12 +156,13 @@ public Object fromAbstract(MapperContext context, AbstractElement element, Class } catch (InstantiationException | IllegalAccessException e) { throw new RuntimeException(e); } catch (AbstractCoercingException ex) { - throw new MapperWrongTypeException(context.getField().getName(), "array", Helpers.typeName(element)); + throw new MapperWrongTypeException(context.getFieldName(), "array", Helpers.typeName(element)); } } public Class[] getSupportedTypes() { return new Class[]{ + Collection.class, List.class, ArrayList.class, CopyOnWriteArrayList.class, @@ -216,7 +216,7 @@ public Object fromAbstract(MapperContext context, AbstractElement element, Class } catch (InstantiationException | IllegalAccessException e) { throw new RuntimeException(e); } catch (AbstractCoercingException ex) { - throw new MapperWrongTypeException(context.getField().getName(), "object", Helpers.typeName(element)); + throw new MapperWrongTypeException(context.getFieldName(), "object", Helpers.typeName(element)); } } @@ -284,7 +284,7 @@ public Object fromAbstract(MapperContext context, AbstractElement element, Class return new Timestamp(date.getTime()); throw new MapperException("Unsupported date type '" + type.getName() + "'"); } catch (ParseException | NumberFormatException | AbstractCoercingException ex) { - throw new MapperException("Failed to parse date '" + element.string() + "'" + (context.getField() != null ? (" for field '" + context.getField().getName() + "'") : "")); + throw new MapperException("Failed to parse date '" + element.string() + "'" + (context.getField() != null ? (" for field '" + context.getFieldName() + "'") : "")); } } @@ -311,13 +311,13 @@ public Object fromAbstract(MapperContext context, AbstractElement element, Class if (type.equals(AbstractElement.class)) return element; if (type.equals(AbstractNull.class) && !(element instanceof AbstractNull)) - throw new MapperWrongTypeException(context.getField().getName(), "null", Helpers.typeName(element)); + throw new MapperWrongTypeException(context.getFieldName(), "null", Helpers.typeName(element)); if (type.equals(AbstractPrimitive.class) && !(element instanceof AbstractPrimitive)) - throw new MapperWrongTypeException(context.getField().getName(), "primitive", Helpers.typeName(element)); + throw new MapperWrongTypeException(context.getFieldName(), "primitive", Helpers.typeName(element)); if (type.equals(AbstractObject.class) && !(element instanceof AbstractObject)) - throw new MapperWrongTypeException(context.getField().getName(), "object", Helpers.typeName(element)); + throw new MapperWrongTypeException(context.getFieldName(), "object", Helpers.typeName(element)); if (type.equals(AbstractArray.class) && !(element instanceof AbstractArray)) - throw new MapperWrongTypeException(context.getField().getName(), "array", Helpers.typeName(element)); + throw new MapperWrongTypeException(context.getFieldName(), "array", Helpers.typeName(element)); return element; } @@ -368,7 +368,7 @@ public AbstractElement toAbstract(MapperContext context, Object value) throws Ma public Object fromAbstract(MapperContext context, AbstractElement element, Class type) throws MapperException { if (type.isEnum()) { if (!element.isString()) - throw new MapperWrongTypeException(context.getField().getName(), "string", Helpers.typeName(element)); + throw new MapperWrongTypeException(context.getFieldName(), "string", Helpers.typeName(element)); try { return Enum.valueOf(type, element.string()); } catch (IllegalArgumentException ex) { @@ -378,7 +378,7 @@ public Object fromAbstract(MapperContext context, AbstractElement element, Class if (type.equals(UUID.class)) return UUID.fromString(element.string()); if (!element.isObject()) - throw new MapperWrongTypeException(context.getField().getName(), "object", Helpers.typeName(element)); + throw new MapperWrongTypeException(context.getFieldName(), "object", Helpers.typeName(element)); MapperTypeSpec spec = MapperTypeSpec.get(type); if (spec == null) throw new MapperException("Unmappable type '" + type.getName() + "'"); diff --git a/src/main/java/org/javawebstack/abstractdata/mapper/Mapper.java b/src/main/java/org/javawebstack/abstractdata/mapper/Mapper.java index 0163804..ad2f235 100644 --- a/src/main/java/org/javawebstack/abstractdata/mapper/Mapper.java +++ b/src/main/java/org/javawebstack/abstractdata/mapper/Mapper.java @@ -41,7 +41,19 @@ public T map(MapperContext context, AbstractElement element, Class type) } if(context.getAdapter() != null) return (T) context.getAdapter().fromAbstract(context, element, type); - return (T) adapters.getOrDefault(type, DefaultMappers.FALLBACK).fromAbstract(context, element, type); + return (T) findAdapter(type).fromAbstract(context, element, type); + } + + private MapperTypeAdapter findAdapter(Class type) { + MapperTypeAdapter adapter = adapters.get(type); + if(adapter != null) + return adapter; + for(Class t : adapters.keySet()) { + if(t.isAssignableFrom(type)) { + return adapters.get(t); + } + } + return DefaultMappers.FALLBACK; } public AbstractElement map(Object obj) throws MapperException { @@ -59,7 +71,7 @@ public AbstractElement map(MapperContext context, Object obj) throws MapperExcep } if(context.getAdapter() != null) return context.getAdapter().toAbstract(context, obj); - return adapters.getOrDefault(obj.getClass(), DefaultMappers.FALLBACK).toAbstract(context, obj); + return findAdapter(obj.getClass()).toAbstract(context, obj); } public Mapper strict() { diff --git a/src/main/java/org/javawebstack/abstractdata/mapper/MapperContext.java b/src/main/java/org/javawebstack/abstractdata/mapper/MapperContext.java index 94492f0..baecb73 100644 --- a/src/main/java/org/javawebstack/abstractdata/mapper/MapperContext.java +++ b/src/main/java/org/javawebstack/abstractdata/mapper/MapperContext.java @@ -34,6 +34,10 @@ public Field getField() { return field; } + public String getFieldName() { + return field != null ? field.getName() : null; + } + public boolean isNested() { return field != null; } diff --git a/src/main/java/org/javawebstack/abstractdata/xml/LegacyXMLParser.java b/src/main/java/org/javawebstack/abstractdata/xml/LegacyXMLParser.java new file mode 100644 index 0000000..ddde196 --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/xml/LegacyXMLParser.java @@ -0,0 +1,59 @@ +package org.javawebstack.abstractdata.xml; + +import org.w3c.dom.*; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/* +This is a temporary implementation using Java's built-in xml implementation + */ +public class LegacyXMLParser { + + @Deprecated + protected static XMLElement parse(String s) { + try { + Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8))); + return convert(document.getDocumentElement()); + } catch (SAXException | IOException | ParserConfigurationException e) { + throw new RuntimeException(e); + } + } + + 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) { + XMLTextNode textNode = (XMLTextNode) node; + return Arrays.asList(escape(textNode.getText(), true)); + } + XMLElement element = (XMLElement) node; + List lines = new ArrayList<>(); + 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) { + lines.add(openingTag + closingTag); + } 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()) { + lines.addAll(dumpLines(child).stream().map(l -> indent + l).collect(Collectors.toList())); + } + lines.add(closingTag); + } + } else { + StringBuilder sb = new StringBuilder(); + sb.append(openingTag); + for(XMLNode child : element.getChildNodes()) + dumpLines(child).forEach(sb::append); + sb.append(closingTag); + lines.add(sb.toString()); + } + return lines; + } + + public String renderOpeningTag(String tagName, Map attributes, boolean selfClosing) { + StringBuilder sb = new StringBuilder(); + sb.append('<'); + sb.append(escape(tagName, false)); + for(String attrName : attributes.keySet()) { + sb.append(" "); + sb.append(escape(attrName, false)); + sb.append('='); + sb.append('"'); + sb.append(escape(attributes.get(attrName), false)); + sb.append('"'); + } + if(selfClosing) + sb.append("/"); + sb.append('>'); + return sb.toString(); + } + + private String escape(String value, boolean text) { + value = value.replace("<", "<"); + value = value.replace("&", "&"); + if(!text) { + value = value.replace("\"", """); + value = value.replace("'", "'"); + value = value.replace(">", ">"); + } + return value; + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/xml/XMLElement.java b/src/main/java/org/javawebstack/abstractdata/xml/XMLElement.java new file mode 100644 index 0000000..c9771ab --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/xml/XMLElement.java @@ -0,0 +1,98 @@ +package org.javawebstack.abstractdata.xml; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class XMLElement implements XMLNode { + + private String tagName; + private final Map attributes = new LinkedHashMap<>(); + private final List childNodes = new ArrayList<>(); + + public XMLElement(String tagName) { + 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) + throw new IllegalArgumentException("text can not be null"); + text(text); + } + + public String tagName() { + return tagName; + } + + public XMLElement tagName(String tagName) { + this.tagName = tagName; + return this; + } + + public String text() { + return childNodes.stream() + .filter(n -> n instanceof XMLTextNode) + .map(n -> ((XMLTextNode) n)) + .map(XMLTextNode::getText) + .collect(Collectors.joining()); + } + + public XMLElement text(String text) { + childNodes.clear(); + childNodes.add(new XMLTextNode(text)); + return this; + } + + public String attr(String name) { + return attributes.get(name); + } + + public XMLElement attr(String name, String value) { + if(value == null) { + attributes.remove(name); + return this; + } + attributes.put(name, value); + return this; + } + + public XMLElement ns(String namespace) { + attr("xmlns", namespace); + return this; + } + + public XMLElement ns(String alias, String namespace) { + attr("xmlns:" + alias, namespace); + return this; + } + + public XMLElement child(XMLNode childNode) { + childNodes.add(childNode); + return this; + } + + public XMLElement onlyIf(boolean condition, Consumer fn) { + if(condition) + fn.accept(this); + return this; + } + + public Map getAttributes() { + return attributes; + } + + public List getChildNodes() { + return childNodes; + } + + public static XMLElement from(String xmlString) { + return new XMLParser().parse(xmlString); + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/xml/XMLNode.java b/src/main/java/org/javawebstack/abstractdata/xml/XMLNode.java new file mode 100644 index 0000000..aef7525 --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/xml/XMLNode.java @@ -0,0 +1,21 @@ +package org.javawebstack.abstractdata.xml; + +public interface XMLNode { + + default String toXML() { + return new XMLDumper().dump(this); + } + + default String toXML(boolean pretty) { + return new XMLDumper().setPretty(pretty).dump(this); + } + + default XMLElement asElement() { + return (XMLElement) this; + } + + default XMLTextNode asTextNode() { + return (XMLTextNode) this; + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/xml/XMLParser.java b/src/main/java/org/javawebstack/abstractdata/xml/XMLParser.java new file mode 100644 index 0000000..da3aa3e --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/xml/XMLParser.java @@ -0,0 +1,9 @@ +package org.javawebstack.abstractdata.xml; + +public class XMLParser { + + public XMLElement parse(String source) { + return LegacyXMLParser.parse(source); + } + +} diff --git a/src/main/java/org/javawebstack/abstractdata/xml/XMLTextNode.java b/src/main/java/org/javawebstack/abstractdata/xml/XMLTextNode.java new file mode 100644 index 0000000..ce63d17 --- /dev/null +++ b/src/main/java/org/javawebstack/abstractdata/xml/XMLTextNode.java @@ -0,0 +1,19 @@ +package org.javawebstack.abstractdata.xml; + +public class XMLTextNode implements XMLNode { + + private String text; + + public XMLTextNode(String text) { + this.text = text; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + +}