summaryrefslogtreecommitdiff
path: root/projects/net.wotonomy.foundation/src/main/java/net
diff options
context:
space:
mode:
Diffstat (limited to 'projects/net.wotonomy.foundation/src/main/java/net')
-rw-r--r--projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSData.java13
-rw-r--r--projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSDate.java9
-rw-r--r--projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSEither.java237
-rw-r--r--projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSError.java71
-rw-r--r--projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSPropertyList.java367
-rw-r--r--projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSPropertyListSerialization.java2
-rw-r--r--projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSXMLPropertyList.java363
-rw-r--r--projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/ReaderInputStream.java19
8 files changed, 1079 insertions, 2 deletions
diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSData.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSData.java
index 3120ad3..ca112c9 100644
--- a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSData.java
+++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSData.java
@@ -23,6 +23,7 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.util.Arrays;
/**
* A pure java implementation of NSData, which is basically a wrapper on a byte
@@ -214,6 +215,18 @@ public class NSData {
return false;
}
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + Arrays.hashCode(bytes);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return isEqual(obj);
+ }
}
/*
diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSDate.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSDate.java
index e3ea753..7a097e4 100644
--- a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSDate.java
+++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSDate.java
@@ -18,6 +18,7 @@ License along with this library; if not, see http://www.gnu.org
package net.wotonomy.foundation;
+import java.time.Instant;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;
@@ -49,6 +50,14 @@ public class NSDate extends Date {
}
/**
+ * Create an NSDate that represents the given instant
+ * @param inst The instant
+ */
+ public NSDate(Instant inst) {
+ super(inst.toEpochMilli());
+ }
+
+ /**
* Represents the specified number of seconds from the current date.
*/
public NSDate(double seconds) {
diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSEither.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSEither.java
new file mode 100644
index 0000000..69714c2
--- /dev/null
+++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSEither.java
@@ -0,0 +1,237 @@
+package net.wotonomy.foundation;
+
+import java.util.*;
+import java.util.function.*;
+
+/**
+ * Represents a choice between objects of two types
+ *
+ * @author bjculkin
+ *
+ * @param <LeftType> The type that could be on the left.
+ *
+ * @param <RightType> The type that could be on the right.
+ *
+ */
+public class NSEither<LeftType, RightType> {
+ /**
+ * Create a new either with the left value occupied.
+ *
+ * @param <LeftType> The type of the left value.
+ *
+ * @param <RightType> The type of the empty right value.
+ *
+ * @param left The value to put on the left.
+ *
+ * @return An either with the left side occupied.
+ */
+ public static <LeftType, RightType> NSEither<LeftType, RightType> left(final LeftType left) {
+ return new NSEither<>(left, null);
+ }
+
+ /**
+ * Create a new either with the right value occupied.
+ *
+ * @param <LeftType> The type of the empty left value.
+ *
+ * @param <RightType> The type of the right value.
+ *
+ * @param right The value to put on the right.
+ *
+ * @return An either with the right side occupied.
+ */
+ public static <LeftType, RightType> NSEither<LeftType, RightType> right(final RightType right) {
+ return new NSEither<>(null, right);
+ }
+
+ /* The left value of the either. */
+ private LeftType leftVal;
+ /* The right value of the either. */
+ private RightType rightVal;
+ /* Whether the left value is the one filled out. */
+ private boolean isLeft;
+
+ /* Create a new either with specifed values. */
+ private NSEither(final LeftType left, final RightType right) {
+ if (left == null) {
+ rightVal = right;
+ } else {
+ leftVal = left;
+
+ isLeft = true;
+ }
+ }
+
+ /**
+ * Perform a mapping over this either.
+ *
+ * @param <NewLeft> The new left type.
+ * @param <NewRight> The new right type.
+ *
+ * @param leftFunc The function to apply if this is a left either.
+ * @param rightFunc The function to apply if this is a right either.
+ *
+ * @return A new either, containing a value transformed by the appropriate
+ * function.
+ */
+ public <NewLeft, NewRight> NSEither<NewLeft, NewRight> map(Function<LeftType, NewLeft> leftFunc,
+ Function<RightType, NewRight> rightFunc) {
+ return isLeft ? left(leftFunc.apply(leftVal)) : right(rightFunc.apply(rightVal));
+ }
+
+ /**
+ * Extract the value from this Either.
+ *
+ * @param <Common> The common type to extract.
+ *
+ * @param leftHandler The function to handle left-values.
+ * @param rightHandler The function to handle right-values.
+ *
+ * @return The result of applying the proper function.
+ */
+ public <Common> Common extract(Function<LeftType, Common> leftHandler, Function<RightType, Common> rightHandler) {
+ return isLeft ? leftHandler.apply(leftVal) : rightHandler.apply(rightVal);
+ }
+
+ /**
+ * Perform an action on this either.
+ *
+ * @param leftHandler The handler of left values.
+ * @param rightHandler The handler of right values.
+ */
+ public void pick(Consumer<LeftType> leftHandler, Consumer<RightType> rightHandler) {
+ if (isLeft)
+ leftHandler.accept(leftVal);
+ else
+ rightHandler.accept(rightVal);
+ }
+
+ /**
+ * Check if this either is left-aligned (has the left value filled, not the
+ * right value).
+ *
+ * @return Whether this either is left-aligned.
+ */
+ public boolean isLeft() {
+ return isLeft;
+ }
+
+ /**
+ * Get the left value of this either if there is one.
+ *
+ * @return An optional containing the left value, if there is one.
+ */
+ public Optional<LeftType> getLeft() {
+ return Optional.ofNullable(leftVal);
+ }
+
+ /**
+ * Get the left value of this either, or get a {@link NoSuchElementException} if
+ * there isn't one.
+ *
+ * @return The left value of this either.
+ *
+ * @throws NoSuchElementException If this either doesn't have a left value.
+ */
+ public LeftType forceLeft() {
+ if (isLeft) {
+ return leftVal;
+ }
+
+ throw new NoSuchElementException("Either has no left value, is right value");
+ }
+
+ /**
+ * Get the right value of this either if there is one.
+ *
+ * @return An optional containing the right value, if there is one.
+ */
+ public Optional<RightType> getRight() {
+ return Optional.ofNullable(rightVal);
+ }
+
+ /**
+ * Get the right value of this either, or get a {@link NoSuchElementException}
+ * if there isn't one.
+ *
+ * @return The right value of this either.
+ *
+ * @throws NoSuchElementException If this either doesn't have a right value.
+ */
+ public RightType forceRight() {
+ if (isLeft) {
+ throw new NoSuchElementException("Either has no right value, has left value");
+ }
+
+ return rightVal;
+ }
+
+ /**
+ * Change the type of the right-side of this either.
+ *
+ * Works only for left Eithers.
+ *
+ * @param <T> The new type for the right side
+ * @return The either with the new type.
+ */
+ @SuppressWarnings("unchecked")
+ public <T> NSEither<LeftType, T> newRight() {
+ if (isLeft) return (NSEither<LeftType, T>) this;
+
+ throw new NoSuchElementException("Can't replace right type on right Either");
+ }
+
+ /**
+ * Change the type of the left-side of this either.
+ *
+ * Works only for right Eithers.
+ *
+ * @param <T> The new type for the left side
+ * @return The either with the new type.
+ */
+ @SuppressWarnings("unchecked")
+ public <T> NSEither<T, RightType> newLeft() {
+ if (isLeft)
+ throw new NoSuchElementException("Can't replace left type on left Either");
+ return (NSEither<T, RightType>) this;
+ }
+
+ /**
+ * Collapse an Either with the same type on both sides.
+ *
+ * @param <T> The type of the either
+ * @param eth The either to collapse
+ *
+ * @return The collapsed either
+ */
+ public static <T> T collapse(NSEither<T, T> eth) {
+ Function<T, T> id = (x) -> x;
+ return eth.extract(id, id);
+ }
+ // Misc. overrides
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(isLeft, leftVal, rightVal);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+
+ NSEither<?, ?> other = (NSEither<?, ?>) obj;
+
+ return isLeft == other.isLeft && Objects.equals(leftVal, other.leftVal)
+ && Objects.equals(rightVal, other.rightVal);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Either [leftVal='%s', rightVal='%s', isLeft=%s]", leftVal, rightVal, isLeft);
+ }
+}
diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSError.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSError.java
new file mode 100644
index 0000000..9e46b5e
--- /dev/null
+++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSError.java
@@ -0,0 +1,71 @@
+package net.wotonomy.foundation;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * Represents an error code.
+ *
+ * @author bjculkin
+ *
+ */
+public class NSError implements Serializable {
+ private static final long serialVersionUID = 532874201592029465L;
+
+ public static final String NSWotonomyDomain = "wotonomy";
+ public static final String NSJavaDomain = "java";
+
+ public final String domain;
+ public final int error;
+
+ public final NSDictionary<String, Object> userInfo;
+
+ private String description;
+
+ private NSArray<NSError> underlyingErrors = new NSMutableArray<>();
+
+ @SuppressWarnings("unchecked")
+ public NSError(String domain, int error) {
+ this(domain, error, (NSDictionary<String, Object>) NSDictionary.EmptyDictionary);
+ }
+
+ public NSError(String domain, int error, NSDictionary<String, Object> userInfo) {
+ this.domain = domain;
+ this.error = error;
+ this.userInfo = userInfo;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public NSArray<NSError> getUnderlyingErrors() {
+ return underlyingErrors;
+ }
+
+ public void addUnderlyingError(NSError underlying) {
+ underlyingErrors.add(underlying);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(domain, error, userInfo, description);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ NSError other = (NSError) obj;
+ return Objects.equals(domain, other.domain) && error == other.error && Objects.equals(userInfo, other.userInfo)
+ && Objects.equals(description, other.description);
+ }
+}
diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSPropertyList.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSPropertyList.java
new file mode 100644
index 0000000..a8cbd3d
--- /dev/null
+++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSPropertyList.java
@@ -0,0 +1,367 @@
+package net.wotonomy.foundation;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+public abstract class NSPropertyList implements Serializable {
+ private static final long serialVersionUID = 2671697049722442864L;
+
+ public static enum Type {
+ ARRAY, DICTIONARY, STRING, DATA, DATE,
+ INTEGER, REAL, BOOL
+ }
+
+ public final Type type;
+
+ protected boolean isImmutable;
+
+ private NSPropertyList(Type type) {
+ this.type = type;
+ }
+
+ public boolean isImmutable() {
+ return isImmutable;
+ }
+
+ public void makeImmutable() {
+ this.isImmutable = true;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(type);
+ }
+
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ NSPropertyList other = (NSPropertyList) obj;
+ return type == other.type;
+ }
+
+ public static final class Array extends NSPropertyList {
+ private static final long serialVersionUID = 7386250174020490701L;
+
+ private NSArray<NSPropertyList> contents;
+
+ public Array(NSArray<NSPropertyList> contents) {
+ super(Type.ARRAY);
+
+ this.contents = contents;
+ }
+
+ public NSArray<NSPropertyList> getContents() {
+ return contents;
+ }
+
+ public boolean setContents(NSArray<NSPropertyList> contents) {
+ if (this.isImmutable) return false;
+
+ this.contents = contents;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = super.hashCode();
+ result = prime * result + Objects.hash(contents);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!super.equals(obj))
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Array other = (Array) obj;
+ return Objects.equals(contents, other.contents);
+ }
+ }
+
+ public static final class Dictionary extends NSPropertyList {
+ private static final long serialVersionUID = 1979462360377516540L;
+
+ private NSDictionary<java.lang.String, NSPropertyList> contents;
+
+ public Dictionary(NSDictionary<java.lang.String,NSPropertyList> retList) {
+ super(Type.DICTIONARY);
+
+ this.contents = retList;
+ }
+
+ public NSDictionary<java.lang.String, NSPropertyList> getContents() {
+ return contents;
+ }
+
+ public boolean setContents(NSDictionary<java.lang.String, NSPropertyList> contents) {
+ if (this.isImmutable) return false;
+
+ this.contents = contents;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = super.hashCode();
+ result = prime * result + Objects.hash(contents);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!super.equals(obj))
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Dictionary other = (Dictionary) obj;
+ return Objects.equals(contents, other.contents);
+ }
+ }
+
+ public static final class String extends NSPropertyList {
+ private static final long serialVersionUID = -388698351414802814L;
+
+ private java.lang.String contents;
+
+ public String(java.lang.String contents) {
+ super(Type.STRING);
+
+ this.contents = contents;
+ }
+
+ public java.lang.String getContents() {
+ return contents;
+ }
+
+ public boolean setContents(java.lang.String contents) {
+ if (this.isImmutable) return false;
+
+ this.contents = contents;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = super.hashCode();
+ result = prime * result + Objects.hash(contents);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!super.equals(obj))
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ String other = (String) obj;
+ return Objects.equals(contents, other.contents);
+ }
+ }
+
+ public static final class Data extends NSPropertyList {
+ private static final long serialVersionUID = -6866410755763823986L;
+ private NSData contents;
+
+ public Data(NSData contents) {
+ super(Type.DATA);
+
+ this.contents = contents;
+ }
+
+ public NSData getContents() {
+ return contents;
+ }
+
+ public boolean setContents(NSData contents) {
+ if (this.isImmutable) return false;
+
+ this.contents = contents;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = super.hashCode();
+ result = prime * result + Objects.hash(contents);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!super.equals(obj))
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Data other = (Data) obj;
+ return Objects.equals(contents, other.contents);
+ }
+ }
+
+ public static final class Date extends NSPropertyList {
+ private static final long serialVersionUID = 6245107872338103662L;
+
+ private NSDate contents;
+
+ public Date(NSDate contents) {
+ super(Type.DATE);
+
+ this.contents = contents;
+ }
+
+ public NSDate getContents() {
+ return contents;
+ }
+
+ public boolean setContents(NSDate contents) {
+ if (this.isImmutable) return false;
+
+ this.contents = contents;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = super.hashCode();
+ result = prime * result + Objects.hash(contents);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!super.equals(obj))
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Date other = (Date) obj;
+ return Objects.equals(contents, other.contents);
+ }
+ }
+
+ public static final class Integer extends NSPropertyList {
+ private static final long serialVersionUID = -6375080842293791774L;
+
+ private int contents;
+
+ public Integer(int contents) {
+ super(Type.INTEGER);
+
+ this.contents = contents;
+ }
+
+ public int getContents() {
+ return contents;
+ }
+
+ public boolean setContents(int contents) {
+ if (this.isImmutable) return false;
+
+ this.contents = contents;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = super.hashCode();
+ result = prime * result + Objects.hash(contents);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!super.equals(obj))
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Integer other = (Integer) obj;
+ return contents == other.contents;
+ }
+ }
+
+ public static final class Real extends NSPropertyList {
+ private static final long serialVersionUID = -4548471713243500294L;
+
+ private double contents;
+
+ public Real(double contents) {
+ super(Type.REAL);
+
+ this.contents = contents;
+ }
+
+ public double getContents() {
+ return contents;
+ }
+
+ public boolean setContents(double contents) {
+ if (this.isImmutable) return false;
+
+ this.contents = contents;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = super.hashCode();
+ result = prime * result + Objects.hash(contents);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!super.equals(obj))
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Real other = (Real) obj;
+ return Double.doubleToLongBits(contents) == Double.doubleToLongBits(other.contents);
+ }
+ }
+
+ public static final class Bool extends NSPropertyList {
+ private static final long serialVersionUID = 1169221814850684398L;
+ private boolean bool;
+
+ public Bool(boolean val) {
+ super(Type.BOOL);
+ }
+
+ public boolean getValue() {
+ return bool;
+ }
+
+ public boolean setValue(boolean bool) {
+ if (this.isImmutable) return false;
+
+ this.bool = bool;
+ return true;
+ }
+ }
+}
diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSPropertyListSerialization.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSPropertyListSerialization.java
index b819662..01a0445 100644
--- a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSPropertyListSerialization.java
+++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSPropertyListSerialization.java
@@ -3,8 +3,6 @@ package net.wotonomy.foundation;
/**
* Class for serializing/unserializing property lists in the .plist format
- *
- *
*/
public class NSPropertyListSerialization {
diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSXMLPropertyList.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSXMLPropertyList.java
new file mode 100644
index 0000000..6881d71
--- /dev/null
+++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSXMLPropertyList.java
@@ -0,0 +1,363 @@
+package net.wotonomy.foundation;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.TemporalAccessor;
+import java.util.Base64;
+import java.util.Base64.Decoder;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.w3c.dom.*;
+import org.xml.sax.SAXException;
+
+import net.wotonomy.foundation.internal.ReaderInputStream;
+import net.wotonomy.foundation.internal.WotonomyException;
+
+/**
+ * Allows reading/writing property lists in the newer XML format instead of
+ * the older text-based one.
+ *
+ * @author bjculkin
+ *
+ */
+public class NSXMLPropertyList {
+ public static enum ErrorCodes {
+ INVALID_ROOT("Invalid root element, must be <plist>"),
+ BAD_PLIST_CHILD_COUNT("<%s> must have one child element", 1),
+ BAD_PLIST_CHILD("child of <%s> must be a %s"),
+ UNKNOWN_NODE_TYPE("unknown node type %s", 1),
+ INVALID_NUM("'%s' is not a valid %s", 2),
+ BAD_COLL_ITEM("Encountered error while parsing %s", 1),
+ MISSING_MAP_KEY("encountered a <%s> while parsing a map, expected <key>", 1);
+
+ public final String desc;
+ public final int numFormatArgs;
+
+ private ErrorCodes(String desc) {
+ this(desc, 0);
+ }
+
+ private ErrorCodes(String desc, int numFormatArgs) {
+ this.desc = desc;
+ this.numFormatArgs = numFormatArgs;
+ }
+ }
+
+ public static enum ImmutabilityType {
+ Immutable, MutableContainers, MutableContainersAndLeaves
+ }
+ /**
+ * Parse a property list from a given string, with the property list
+ * being immutable.
+ *
+ * @param s The string to parse the property list from
+ * @return The immutable property list
+ */
+ public static NSEither<NSPropertyList, NSError> propertyListFromString(String s) {
+ return propertyListFromString(s, ImmutabilityType.Immutable);
+ }
+
+ public static NSEither<NSPropertyList, NSError> propertyListFromString(String s, ImmutabilityType immutable) {
+ StringReader sReader = new StringReader(s);
+ ReaderInputStream inp = new ReaderInputStream(sReader);
+ return propertyListFromStream(inp, immutable);
+ }
+
+ public static NSEither<NSPropertyList, NSError> propertyListFromStream(InputStream inp) {
+ return propertyListFromStream(inp, ImmutabilityType.Immutable);
+ }
+
+ public static NSEither<NSPropertyList, NSError> propertyListFromStream(InputStream inp, ImmutabilityType immutable) {
+ try {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ // TODO allow attaching comments to each element
+ factory.setIgnoringComments(true);
+ DocumentBuilder fact = factory.newDocumentBuilder();
+ Document doc = fact.parse(inp);
+ Element plistRoot = doc.getDocumentElement();
+ plistRoot.normalize();
+
+ if (!plistRoot.getTagName().equals("plist")) {
+ return createError(ErrorCodes.INVALID_ROOT);
+ }
+
+ NodeList children = plistRoot.getChildNodes();
+ // TODO this needs to be adjusted to allow comments to exist
+ if (children.getLength() != 1) {
+ return createError(ErrorCodes.BAD_PLIST_CHILD_COUNT, "plist");
+ }
+
+ Node initial = children.item(0);
+ if (initial.getNodeType() != Node.ELEMENT_NODE) {
+ return createError(ErrorCodes.BAD_PLIST_CHILD, "plist", "tag");
+ }
+
+ return parsePlistValue(initial, immutable);
+ } catch (ParserConfigurationException pcex) {
+ throw new WotonomyException("Failed to create parser", pcex);
+ } catch (SAXException saxex) {
+ throw new RuntimeException("Failed to read XML", saxex);
+ } catch (IOException ioex) {
+ throw new RuntimeException("Error parsing XML", ioex);
+ }
+ }
+
+ private static NSEither<NSPropertyList, NSError> parsePlistValue(Node nod, ImmutabilityType immutable) {
+ String initNodeName = nod.getNodeName();
+ switch(initNodeName) {
+ case "array":
+ return arrayFromNode(nod, immutable);
+ case "dict":
+ return dictFromNode(nod, immutable);
+ case "string": {
+ NodeList stringKid = nod.getChildNodes();
+ if (stringKid.getLength() != 1) return createError(ErrorCodes.BAD_PLIST_CHILD_COUNT, "string");
+ Node text = stringKid.item(0);
+ if (text.getNodeType() != Node.TEXT_NODE) return createError(ErrorCodes.BAD_PLIST_CHILD, "string", "text");
+ NSPropertyList.String ret = new NSPropertyList.String(text.getNodeValue());
+ if (immutable != ImmutabilityType.MutableContainersAndLeaves) ret.makeImmutable();
+ return NSEither.left(ret);
+ }
+
+ case "data": {
+ Decoder decoder = Base64.getDecoder();
+ NodeList stringKid = nod.getChildNodes();
+ if (stringKid.getLength() != 1) return createError(ErrorCodes.BAD_PLIST_CHILD_COUNT, "data");
+ Node text = stringKid.item(0);
+ if (text.getNodeType() != Node.TEXT_NODE) return createError(ErrorCodes.BAD_PLIST_CHILD, "data", "text");
+ String raw = text.getNodeValue();
+ NSData dat = new NSData(decoder.decode(raw));
+ NSPropertyList.Data ret = new NSPropertyList.Data(dat);
+ if (immutable != ImmutabilityType.MutableContainersAndLeaves) ret.makeImmutable();
+ return NSEither.left(ret);
+ }
+ case "date": {
+ // TODO determine what the correct default date format is
+ // Believe it is ISO_INSTANT from the documentation I have read
+ DateTimeFormatter dateFmt = DateTimeFormatter.ISO_INSTANT;
+ NamedNodeMap attributes = nod.getAttributes();
+ Node formatNode = attributes.getNamedItem("format");
+ // Because we got it from the attribute map, it has to be
+ // an attribute
+ if (formatNode != null) {
+ Attr formatAttr = (Attr) formatNode;
+ String format = formatAttr.getValue();
+ dateFmt = DateTimeFormatter.ofPattern(format);
+ }
+
+ // TODO abstract this chunk into a method
+ NodeList stringKid = nod.getChildNodes();
+ if (stringKid.getLength() != 1) return createError(ErrorCodes.BAD_PLIST_CHILD_COUNT, "date");
+ Node text = stringKid.item(0);
+ if (text.getNodeType() != Node.TEXT_NODE) return createError(ErrorCodes.BAD_PLIST_CHILD, "date", "text");
+ String val = text.getNodeValue();
+
+ TemporalAccessor tempDate = dateFmt.parse(val);
+ Instant inst = Instant.from(tempDate);
+
+ NSPropertyList.Date ret = new NSPropertyList.Date(new NSDate(inst));
+ if (immutable != ImmutabilityType.MutableContainersAndLeaves) ret.makeImmutable();
+ return NSEither.left(ret);
+ }
+ case "integer": {
+ NodeList stringKid = nod.getChildNodes();
+ if (stringKid.getLength() != 1) return createError(ErrorCodes.BAD_PLIST_CHILD_COUNT, "integer");
+ Node text = stringKid.item(0);
+ if (text.getNodeType() != Node.TEXT_NODE) return createError(ErrorCodes.BAD_PLIST_CHILD, "integer", "text");
+ String val = text.getNodeValue();
+
+ try {
+ int ret = Integer.parseInt(val);
+
+ NSPropertyList.Integer res = new NSPropertyList.Integer(ret);
+ if (immutable != ImmutabilityType.MutableContainersAndLeaves) res.makeImmutable();
+ return NSEither.left(res);
+ } catch (NumberFormatException nfex) {
+ return createError(ErrorCodes.INVALID_NUM, val, "integer");
+ }
+ }
+ case "real": {
+ NodeList stringKid = nod.getChildNodes();
+ if (stringKid.getLength() != 1) return createError(ErrorCodes.BAD_PLIST_CHILD_COUNT, "real");
+ Node text = stringKid.item(0);
+ if (text.getNodeType() != Node.TEXT_NODE) return createError(ErrorCodes.BAD_PLIST_CHILD, "real", "text");
+ String val = text.getNodeValue();
+
+ try {
+ double ret = Double.parseDouble(val);
+
+ NSPropertyList.Real res = new NSPropertyList.Real(ret);
+ if (immutable != ImmutabilityType.MutableContainersAndLeaves) res.makeImmutable();
+ return NSEither.left(res);
+ } catch (NumberFormatException nfex) {
+ return createError(ErrorCodes.INVALID_NUM, val, "double");
+ }
+ }
+ case "true": {
+ NSPropertyList.Bool res = new NSPropertyList.Bool(true);
+ if (immutable != ImmutabilityType.MutableContainersAndLeaves) res.makeImmutable();
+ return NSEither.left(res);
+ }
+ case "false": {
+ NSPropertyList.Bool res = new NSPropertyList.Bool(false);
+ if (immutable != ImmutabilityType.MutableContainersAndLeaves) res.makeImmutable();
+ return NSEither.left(res);
+ }
+ default:
+ return createError(ErrorCodes.UNKNOWN_NODE_TYPE, initNodeName);
+ }
+ }
+
+ private static NSEither<NSPropertyList, NSError> dictFromNode(Node nod, ImmutabilityType immutable) {
+ NodeList lst = nod.getChildNodes();
+ int numChild = lst.getLength();
+
+ boolean hasError = false;
+ NSDictionary<String, NSPropertyList> retDict = new NSMutableDictionary<>();
+ NSError retError = createRawError(ErrorCodes.BAD_COLL_ITEM, "dictionary");
+
+ for (int i = 0; i < numChild; i++) {
+ Node kid = lst.item(i);
+ short typ = kid.getNodeType();
+ if (typ == Node.COMMENT_NODE || typ == Node.PROCESSING_INSTRUCTION_NODE) continue;
+
+ if (typ != Node.ELEMENT_NODE) {
+ hasError = true;
+ // Skip over the corresponding value, since we don't have a key for it
+ i++;
+ retError.addUnderlyingError(createRawError(ErrorCodes.BAD_PLIST_CHILD, "dict", "tag"));
+ continue;
+ }
+
+ // First, grab the key, then we can handle the value
+ if (!kid.getNodeName().equals("key")) {
+ hasError = true;
+ // Skip over the corresponding value, since we don't have a key for it
+ i++;
+ retError.addUnderlyingError(createRawError(ErrorCodes.MISSING_MAP_KEY, kid.getNodeName()));
+ continue;
+ }
+
+ NodeList stringKid = kid.getChildNodes();
+
+ if (stringKid.getLength() != 1) {
+ hasError = true;
+ // Skip over the corresponding value, since we don't have a key for it
+ i++;
+ retError.addUnderlyingError(createRawError(ErrorCodes.BAD_PLIST_CHILD_COUNT, "key"));
+ continue;
+ }
+
+ Node text = stringKid.item(0);
+
+ if (text.getNodeType() != Node.TEXT_NODE) {
+ hasError = true;
+ // Skip over the corresponding value, since we don't have a key for it
+ i++;
+ retError.addUnderlyingError(createRawError(ErrorCodes.BAD_PLIST_CHILD, "key", "text"));
+ continue;
+ }
+
+ String key = text.getNodeValue();
+ i++;
+
+ kid = lst.item(i);
+ typ = kid.getNodeType();
+ if (typ == Node.COMMENT_NODE || typ == Node.PROCESSING_INSTRUCTION_NODE) continue;
+
+ if (typ != Node.ELEMENT_NODE) {
+ hasError = true;
+ retError.addUnderlyingError(createRawError(ErrorCodes.BAD_PLIST_CHILD, "array", "tag"));
+ continue;
+ }
+
+ var res = parsePlistValue(kid, immutable);
+ if (res.isLeft()) {
+ retDict.put(key, res.forceLeft());
+ } else {
+ hasError = true;
+ retError.addUnderlyingError(res.forceRight());
+ }
+ }
+
+
+ if (hasError) {
+ return NSEither.right(retError);
+ } else {
+ NSPropertyList.Dictionary res;
+ if (immutable == ImmutabilityType.Immutable) {
+ res = new NSPropertyList.Dictionary(new NSDictionary<>(retDict));
+ res.makeImmutable();
+ } else {
+ res = new NSPropertyList.Dictionary(new NSDictionary<>(retDict));
+ }
+ return NSEither.left(res);
+ }
+ }
+
+ private static NSEither<NSPropertyList, NSError> arrayFromNode(Node nod, ImmutabilityType immutable) {
+ NodeList lst = nod.getChildNodes();
+ int numChild = lst.getLength();
+
+ boolean hasError = false;
+ NSArray<NSPropertyList> retList = new NSMutableArray<>();
+ NSError retError = createRawError(ErrorCodes.BAD_COLL_ITEM, "array");
+
+ for (int i = 0; i < numChild; i++) {
+ Node kid = lst.item(i);
+ short typ = kid.getNodeType();
+ if (typ == Node.COMMENT_NODE || typ == Node.PROCESSING_INSTRUCTION_NODE) continue;
+
+ if (typ != Node.ELEMENT_NODE) {
+ hasError = true;
+ retError.addUnderlyingError(createRawError(ErrorCodes.BAD_PLIST_CHILD, "array", "tag"));
+ continue;
+ }
+
+ var res = parsePlistValue(kid, immutable);
+ if (res.isLeft()) {
+ retList.add(res.forceLeft());
+ } else {
+ hasError = true;
+ retError.addUnderlyingError(res.forceRight());
+ }
+ }
+
+ if (hasError) {
+ return NSEither.right(retError);
+ } else {
+ NSPropertyList.Array arr;
+ if (immutable == ImmutabilityType.Immutable) {
+ arr = new NSPropertyList.Array(new NSArray<>(retList));
+ arr.makeImmutable();
+ } else {
+ arr = new NSPropertyList.Array(retList);
+ }
+ return NSEither.left(arr);
+ }
+ }
+
+ private static NSEither<NSPropertyList, NSError> createError(ErrorCodes errorCode, Object... formatArgs) {
+ NSError error = createRawError(errorCode, formatArgs);
+
+ return NSEither.right(error);
+ }
+
+ private static NSError createRawError(ErrorCodes errorCode, Object... formatArgs) {
+ if (formatArgs.length != errorCode.numFormatArgs) {
+ String fmt = "Incorrect number of format args for error code %s; got %d, expected %d";
+ String msg = String.format(fmt, errorCode.name(), formatArgs.length, errorCode.numFormatArgs);
+ throw new WotonomyException(msg);
+ }
+
+ NSError error = new NSError(NSError.NSWotonomyDomain, errorCode.ordinal());
+ error.setDescription(String.format(errorCode.desc, formatArgs));
+ return error;
+ }
+}
diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/ReaderInputStream.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/ReaderInputStream.java
new file mode 100644
index 0000000..48f8b55
--- /dev/null
+++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/ReaderInputStream.java
@@ -0,0 +1,19 @@
+package net.wotonomy.foundation.internal;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+
+public class ReaderInputStream extends InputStream {
+ private Reader rdr;
+
+ public ReaderInputStream(Reader rdr) {
+ this.rdr = rdr;
+ }
+
+ @Override
+ public int read() throws IOException {
+ return rdr.read();
+ }
+
+}