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 "), 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 ", 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 propertyListFromString(String s) { return propertyListFromString(s, ImmutabilityType.Immutable); } public static NSEither propertyListFromString(String s, ImmutabilityType immutable) { StringReader sReader = new StringReader(s); ReaderInputStream inp = new ReaderInputStream(sReader); return propertyListFromStream(inp, immutable); } public static NSEither propertyListFromStream(InputStream inp) { return propertyListFromStream(inp, ImmutabilityType.Immutable); } public static NSEither 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 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 dictFromNode(Node nod, ImmutabilityType immutable) { NodeList lst = nod.getChildNodes(); int numChild = lst.getLength(); boolean hasError = false; NSDictionary 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 arrayFromNode(Node nod, ImmutabilityType immutable) { NodeList lst = nod.getChildNodes(); int numChild = lst.getLength(); boolean hasError = false; NSArray 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 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; } }