diff options
| author | Benjamin Culkin <scorpress@gmail.com> | 2024-07-08 17:30:58 -0400 |
|---|---|---|
| committer | Benjamin Culkin <scorpress@gmail.com> | 2024-07-08 17:30:58 -0400 |
| commit | 9c681f38b742b26b841eb42bc19879cb90ac03de (patch) | |
| tree | 6c1b9eb1971629cc3c42bae3bff9fd930d4af11c /projects/net.wotonomy.foundation/src/main/java | |
| parent | 6d46c473d41c6c47e6b8bd8c676d925e544bd378 (diff) | |
Add XML property lists
Implement support for the XML property lists that are the newer version
of the ASCII ones. There are a few things that still need to be done,
but all of the basics are there
Next things
- Allow collapsing a property list into a series of objects
- Serialize both the property list and flattened objects to XML
Diffstat (limited to 'projects/net.wotonomy.foundation/src/main/java')
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(); + } + +} |
