
src.main.java.com.dd.plist.ASCIIPropertyListParser Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of dd-plist Show documentation
Show all versions of dd-plist Show documentation
This library enables Java applications to work with property lists in various formats.
Supported formats for reading and writing are OS X/iOS binary and XML property lists.
ASCII property lists are also supported.
The library also provides access to basic functions of NeXTSTEP/Cocoa classes like
NSDictionary, NSArray, etc.
The newest version!
/*
* plist - An open source library to parse and generate property lists
* Copyright (C) 2014 Daniel Dreibrodt
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.dd.plist;
import java.io.CharArrayWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.ParseException;
import java.text.StringCharacterIterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
/**
*
* Parser for ASCII property lists. Supports Apple OS X/iOS and GnuStep/NeXTSTEP format. This parser
* is based on the recursive descent paradigm, but the underlying grammar is not explicitly
* defined.
*
*
* Resources on ASCII property list format:
*
*
* -
* Property List Programming Guide - Old-Style ASCII Property Lists
*
* -
* GnuStep - NSPropertyListSerialization class documentation
*
*
*
* @author Daniel Dreibrodt
*/
public final class ASCIIPropertyListParser {
/**
* The space character token.
*/
public static final char WHITESPACE_SPACE = ' ';
/**
* The tab character token.
*/
public static final char WHITESPACE_TAB = '\t';
/**
* The newline character token.
*/
public static final char WHITESPACE_NEWLINE = '\n';
/**
* The carriage return character token.
*/
public static final char WHITESPACE_CARRIAGE_RETURN = '\r';
/**
* The token marking the beginning of an array.
*/
public static final char ARRAY_BEGIN_TOKEN = '(';
/**
* The token marking the end of an array.
*/
public static final char ARRAY_END_TOKEN = ')';
/**
* The token marking the end of an array element.
*/
public static final char ARRAY_ITEM_DELIMITER_TOKEN = ',';
/**
* The token marking the beginning of a dictionary.
*/
public static final char DICTIONARY_BEGIN_TOKEN = '{';
/**
* The token marking the end of a dictionary.
*/
public static final char DICTIONARY_END_TOKEN = '}';
/**
* The token marking the assignment of a value to a dictionary key.
*/
public static final char DICTIONARY_ASSIGN_TOKEN = '=';
/**
* The token marking the end of a dictionary entry.
*/
public static final char DICTIONARY_ITEM_DELIMITER_TOKEN = ';';
/**
* The token marking the beginning of a quoted string.
*/
public static final char QUOTEDSTRING_BEGIN_TOKEN = '"';
/**
* The token marking the end of a quoted string.
*/
public static final char QUOTEDSTRING_END_TOKEN = '"';
/**
* The token marking the beginning of an escape sequence in a quoted string.
*/
public static final char QUOTEDSTRING_ESCAPE_TOKEN = '\\';
/**
* The token marking the beginning of a data element.
*/
public static final char DATA_BEGIN_TOKEN = '<';
/**
* The token marking the end of a data element.
*/
public static final char DATA_END_TOKEN = '>';
/**
* The token marking the beginning of a data element in Base-64 encoding.
*/
public static final char DATA_BASE64_BEGIN_TOKEN = '[';
/**
* The token marking the end of a data element in Base-64 encoding.
*/
public static final char DATA_BASE64_END_TOKEN = ']';
/**
* The token marking the beginning of a GnuStep object.
*/
public static final char DATA_GSOBJECT_BEGIN_TOKEN = '*';
/**
* The token marking the beginning of a GnuStep date.
*/
public static final char DATA_GSDATE_BEGIN_TOKEN = 'D';
/**
* The token marking the beginning of a GnuStep boolean value.
*/
public static final char DATA_GSBOOL_BEGIN_TOKEN = 'B';
/**
* The token representing the boolean value {@code true} in the GnuStep format.
*/
public static final char DATA_GSBOOL_TRUE_TOKEN = 'Y';
/**
* The token representing the boolean value {@code false} in the GnuStep format.
*/
public static final char DATA_GSBOOL_FALSE_TOKEN = 'N';
/**
* The token marking the beginning of a GnuStep integer value.
*/
public static final char DATA_GSINT_BEGIN_TOKEN = 'I';
/**
* The token marking the beginning of a GnuStep real value.
*/
public static final char DATA_GSREAL_BEGIN_TOKEN = 'R';
/**
* The token that separates the parts of a date value (year, month and day).
*/
public static final char DATE_DATE_FIELD_DELIMITER = '-';
/**
* The token that separates the parts of a time value (hour, minute and second).
*/
public static final char DATE_TIME_FIELD_DELIMITER = ':';
/**
* The token that separates the date and time in the GnuStep format.
*/
public static final char DATE_GS_DATE_TIME_DELIMITER = ' ';
/**
* The token that marks the beginning of the time zone in the Apple format.
*/
public static final char DATE_APPLE_DATE_TIME_DELIMITER = 'T';
/**
* The token that marks the end of the time zone in the Apple format.
*/
public static final char DATE_APPLE_END_TOKEN = 'Z';
/**
* The token marking the beginning of a comment.
*/
public static final char COMMENT_BEGIN_TOKEN = '/';
/**
* The token marking a comment to be multi-line.
*/
public static final char MULTILINE_COMMENT_SECOND_TOKEN = '*';
/**
* The token marking a comment to be single-line.
*/
public static final char SINGLELINE_COMMENT_SECOND_TOKEN = '/';
/**
* The token marking the end of a multi-line comment. Must be preceded by a
* {@link ASCIIPropertyListParser#MULTILINE_COMMENT_SECOND_TOKEN}.
*/
public static final char MULTILINE_COMMENT_END_TOKEN = '/';
/**
* Property list source data
*/
private final char[] data;
/**
* Current parsing index
*/
private int index;
/**
* Current line number.
*/
private int lineNo = 1;
/**
* The index at which the current line began.
*/
private int lineBeginning = -1;
/**
* Creates a new parser for the given property list content.
*
* @param propertyListContent The content of the property list that is to be parsed.
* @param encoding The name of a supported {@link java.nio.charset.Charset charset} to
* decode the property list.
* @throws java.io.UnsupportedEncodingException If no support for the named charset is available
* in this instance of the Java virtual machine.
*/
private ASCIIPropertyListParser(byte[] propertyListContent, String encoding)
throws UnsupportedEncodingException {
this(new String(propertyListContent, encoding).toCharArray());
}
/**
* Creates a new parser for the given property list content.
*
* @param propertyListContent The content of the property list that is to be parsed.
*/
private ASCIIPropertyListParser(char[] propertyListContent) {
this.data = propertyListContent;
}
/**
* Parses an ASCII property list file.
*
* @param f The ASCII property list file.
* @return The root object of the property list. This is usually a {@link NSDictionary} but can
* also be a {@link NSArray}.
* @throws java.text.ParseException If an error occurs during parsing.
* @throws java.io.IOException If an error occurs while reading from the input stream.
*/
public static NSObject parse(File f) throws IOException, ParseException {
return parse(f.toPath());
}
/**
* Parses an ASCII property list file.
*
* @param f The ASCII property list file.
* @param encoding The name of a supported {@link java.nio.charset.Charset charset} to decode the
* property list.
* @return The root object of the property list. This is usually a {@link NSDictionary} but can
* also be a {@link NSArray}.
* @throws java.text.ParseException If an error occurs during parsing.
* @throws java.io.IOException If an error occurs while reading from the input
* stream.
* @throws java.io.UnsupportedEncodingException If no support for the named charset is available
* in this instance of the Java virtual machine.
*/
public static NSObject parse(File f, String encoding) throws IOException, ParseException {
return parse(f.toPath(), encoding);
}
/**
* Parses an ASCII property list file.
*
* @param path The path to the ASCII property list file.
* @param encoding The name of a supported {@link java.nio.charset.Charset charset} to decode the
* property list.
* @return The root object of the property list. This is usually a {@link NSDictionary} but can
* also be a {@link NSArray}.
* @throws java.text.ParseException If an error occurs during parsing.
* @throws java.io.IOException If an error occurs while reading from the input
* stream.
* @throws java.io.UnsupportedEncodingException If no support for the named charset is available
* in this instance of the Java virtual machine.
*/
public static NSObject parse(Path path, String encoding) throws IOException, ParseException {
try (InputStream fileInputStream = Files.newInputStream(path)) {
return parse(fileInputStream, encoding);
}
}
/**
* Parses an ASCII property list file.
*
* @param path The path to the ASCII property list file.
* @return The root object of the property list. This is usually a {@link NSDictionary} but can
* also be a {@link NSArray}.
* @throws java.text.ParseException If an error occurs during parsing.
* @throws java.io.IOException If an error occurs while reading from the input stream.
*/
public static NSObject parse(Path path) throws IOException, ParseException {
try (InputStream fileInputStream = Files.newInputStream(path)) {
return parse(fileInputStream);
}
}
/**
* Parses an ASCII property list from an input stream. This method does not close the specified
* input stream.
*
* @param in The input stream that provides the property list's data.
* @return The root object of the property list. This is usually a {@link NSDictionary} but can
* also be a {@link NSArray}.
* @throws java.text.ParseException If an error occurs during parsing.
* @throws java.io.IOException If an error occurs while reading from the input stream.
*/
public static NSObject parse(InputStream in) throws ParseException, IOException {
return parse(PropertyListParser.readAll(in));
}
/**
* Parses an ASCII property list from an input stream. This method does not close the specified
* input stream.
*
* @param in The input stream that points to the property list's data.
* @param encoding The name of a supported {@link java.nio.charset.Charset charset} to decode the
* property list.
* @return The root object of the property list. This is usually a {@link NSDictionary} but can
* also be a {@link NSArray}.
* @throws java.text.ParseException If an error occurs during parsing.
* @throws java.io.IOException If an error occurs while reading from the input
* stream.
* @throws java.io.UnsupportedEncodingException If no support for the named charset is available
* in this instance of the Java virtual machine.
*/
public static NSObject parse(InputStream in, String encoding) throws ParseException, IOException {
return parse(PropertyListParser.readAll(in), encoding);
}
/**
* Parses an ASCII property list from a {@link Reader}. This method does not close the specified
* reader.
*
* @param reader The reader that provides the property list's data.
* @return The root object of the property list. This is usually a {@link NSDictionary} but can
* also be a {@link NSArray}.
* @throws java.text.ParseException If an error occurs during parsing.
* @throws java.io.IOException If an error occurs while reading from the input reader.
*/
public static NSObject parse(Reader reader) throws ParseException, IOException {
Objects.requireNonNull(reader, "The specified reader is null");
CharArrayWriter charArrayWriter = new CharArrayWriter();
char[] buf = new char[4096];
int read;
while ((read = reader.read(buf)) >= 0) {
charArrayWriter.write(buf, 0, read);
}
ASCIIPropertyListParser parser = new ASCIIPropertyListParser(charArrayWriter.toCharArray());
return parser.parse();
}
/**
* Parses an ASCII property list from a {@link String}
*
* @param plistData A string containing the property list's data.
* @return The root object of the property list. This is usually a {@link NSDictionary} but can
* also be a {@link NSArray}.
* @throws java.text.ParseException If an error occurs during parsing.
*/
public static NSObject parse(String plistData) throws ParseException {
ASCIIPropertyListParser parser = new ASCIIPropertyListParser(plistData.toCharArray());
return parser.parse();
}
/**
* Parses an ASCII property list from a byte array.
*
* @param bytes The ASCII property list data.
* @return The root object of the property list. This is usually a {@link NSDictionary} but can
* also be a {@link NSArray}.
* @throws ParseException If an error occurs during parsing.
*/
public static NSObject parse(byte[] bytes) throws ParseException {
String charset = ByteOrderMarkReader.detect(bytes);
if (charset == null) {
charset = "UTF-8";
}
try {
return parse(bytes, charset);
} catch (UnsupportedEncodingException e) {
// Unlikely to happen as only standard codepages are requested
throw new RuntimeException(
"Unsupported property list encoding (" + charset + "): " + e.getMessage());
}
}
/**
* Parses an ASCII property list from a byte array.
*
* @param bytes The ASCII property list data.
* @param encoding The name of a supported {@link java.nio.charset.Charset} charset to decode the
* property list.
* @return The root object of the property list. This is usually a {@link NSDictionary} but can
* also be a {@link NSArray}.
* @throws ParseException If an error occurs during parsing.
* @throws java.io.UnsupportedEncodingException If no support for the named charset is available
* in this instance of the Java virtual machine.
*/
public static NSObject parse(byte[] bytes, String encoding)
throws ParseException, UnsupportedEncodingException {
ASCIIPropertyListParser parser = new ASCIIPropertyListParser(bytes, encoding);
return parser.parse();
}
/**
* Checks whether the given sequence of symbols can be accepted.
*
* @param sequence The sequence of tokens to look for.
* @return Whether the given tokens occur at the current parsing position.
*/
private boolean acceptSequence(char... sequence) {
if (this.index + sequence.length > this.data.length) {
return false;
}
for (int i = 0; i < sequence.length; i++) {
if (this.data[this.index + i] != sequence[i]) {
return false;
}
}
return true;
}
/**
* Checks whether the given symbols can be accepted, that is, if one of the given symbols is found
* at the current parsing position.
*
* @param acceptableSymbols The symbols to check.
* @return Whether one of the symbols can be accepted or not.
*/
private boolean accept(char... acceptableSymbols) {
boolean symbolPresent = false;
if (this.index < this.data.length) {
for (char c : acceptableSymbols) {
if (this.data[this.index] == c) {
symbolPresent = true;
break;
}
}
}
return symbolPresent;
}
/**
* Checks whether the given symbol can be accepted, that is, if the given symbols is found at the
* current parsing position.
*
* @param acceptableSymbol The symbol to check.
* @return Whether the symbol can be accepted or not.
*/
private boolean accept(char acceptableSymbol) {
return this.index < this.data.length && this.data[this.index] == acceptableSymbol;
}
/**
* Expects the input to have one of the given symbols at the current parsing position.
*
* @param expectedSymbols The expected symbols.
* @throws ParseException If none of the expected symbols could be found.
*/
private void expect(char... expectedSymbols) throws ParseException {
if (!this.accept(expectedSymbols)) {
StringBuilder excString = new StringBuilder();
excString.append("Expected '").append(expectedSymbols[0]).append("'");
for (int i = 1; i < expectedSymbols.length; i++) {
excString.append(" or '").append(expectedSymbols[i]).append("'");
}
if (this.index < this.data.length) {
excString.append(" but found '").append(this.data[this.index]).append("'");
} else {
excString.append(" but reached end of input");
}
throw this.createParseException(excString.toString(), this.index);
}
}
/**
* Expects the input to have the given symbol at the current parsing position.
*
* @param expectedSymbol The expected symbol.
* @throws ParseException If the expected symbol could not be found.
*/
private void expect(char expectedSymbol) throws ParseException {
if (!this.accept(expectedSymbol)) {
throw this.createParseException(
this.index < this.data.length
? "Expected '" + expectedSymbol + "' but found '" + this.data[this.index] + "'"
: "Expected '" + expectedSymbol + "' but reached end of input",
this.index);
}
}
/**
* Reads an expected symbol.
*
* @param symbol The symbol to read.
* @throws ParseException If the expected symbol could not be read.
*/
private void read(char symbol) throws ParseException {
this.expect(symbol);
this.index++;
}
/**
* Skips the current symbol.
*/
private void skip() {
this.index++;
}
/**
* Skips several symbols
*
* @param numSymbols The amount of symbols to skip.
*/
private void skip(int numSymbols) {
this.index += numSymbols;
}
private void trackLineBreak() {
if (this.data[this.index] == WHITESPACE_NEWLINE) {
// \n or \r\n
this.lineNo++;
this.lineBeginning = this.index;
}
if (this.data[this.index] == WHITESPACE_CARRIAGE_RETURN
&& !(this.index + 1 < this.data.length
&& this.data[this.index + 1] == WHITESPACE_NEWLINE)) {
// Single \r
this.lineNo++;
this.lineBeginning = this.index;
}
}
/**
* Skips all whitespaces and comments from the current parsing position onward.
*/
private void skipWhitespacesAndComments() {
boolean commentSkipped;
do {
commentSkipped = false;
//Skip whitespaces
while (this.accept(WHITESPACE_CARRIAGE_RETURN, WHITESPACE_NEWLINE, WHITESPACE_SPACE,
WHITESPACE_TAB)) {
this.trackLineBreak();
this.skip();
}
//Skip single line comments "//..."
if (this.acceptSequence(COMMENT_BEGIN_TOKEN, SINGLELINE_COMMENT_SECOND_TOKEN)) {
this.skip(2);
this.readInputUntil(WHITESPACE_CARRIAGE_RETURN, WHITESPACE_NEWLINE);
commentSkipped = true;
}
//Skip multi line comments "/* ... */"
else if (this.acceptSequence(COMMENT_BEGIN_TOKEN, MULTILINE_COMMENT_SECOND_TOKEN)) {
this.skip(2);
while (this.index < this.data.length) {
if (this.acceptSequence(MULTILINE_COMMENT_SECOND_TOKEN, MULTILINE_COMMENT_END_TOKEN)) {
this.skip(2);
break;
}
this.trackLineBreak();
this.skip();
}
commentSkipped = true;
}
}
while (commentSkipped); //if a comment was skipped more whitespace or another comment can follow, so skip again
}
/**
* Reads input until one of the given symbols is found.
*
* @param symbols The symbols that can occur after the string to read.
* @return The input until one the given symbols.
*/
private String readInputUntil(char... symbols) {
StringBuilder strBuf = new StringBuilder();
while (this.index < this.data.length && !this.accept(symbols)) {
strBuf.append(this.data[this.index]);
this.skip();
}
return strBuf.toString();
}
/**
* Reads input until the given symbol is found.
*
* @param symbol The symbol that can occur after the string to read.
* @return The input until the given symbol.
*/
private String readInputUntil(char symbol) {
StringBuilder strBuf = new StringBuilder();
while (this.index < this.data.length && !this.accept(symbol)) {
strBuf.append(this.data[this.index]);
this.trackLineBreak();
this.skip();
}
return strBuf.toString();
}
/**
* Parses the property list from the beginning and returns the root object of the property list.
*
* @return The root object of the property list. This can either be a NSDictionary or a NSArray.
* @throws ParseException If an error occurred during parsing
*/
public NSObject parse() throws ParseException {
this.index = 0;
if (this.data.length == 0) {
throw new ParseException("The property list is empty.", 0);
}
//Skip Unicode byte order mark (BOM)
if (this.data[0] == '\uFEFF') {
this.skip(1);
}
this.skipWhitespacesAndComments();
this.expect(DICTIONARY_BEGIN_TOKEN, ARRAY_BEGIN_TOKEN, COMMENT_BEGIN_TOKEN);
try {
return this.parseObject();
} catch (ArrayIndexOutOfBoundsException ex) {
throw this.createParseException("Reached end of input unexpectedly.", this.index);
}
}
/**
* Parses the NSObject found at the current position in the property list data stream.
*
* @return The parsed NSObject.
* @see ASCIIPropertyListParser#index
*/
private NSObject parseObject() throws ParseException {
LocationInformation loc = new ASCIILocationInformation(this.index, this.lineNo,
this.index - this.lineBeginning);
NSObject result;
switch (this.data[this.index]) {
case ARRAY_BEGIN_TOKEN: {
result = this.parseArray();
break;
}
case DICTIONARY_BEGIN_TOKEN: {
result = this.parseDictionary();
break;
}
case DATA_BEGIN_TOKEN: {
result = this.parseData();
break;
}
case QUOTEDSTRING_BEGIN_TOKEN: {
String quotedString = this.parseQuotedString();
//apple dates are quoted strings of length 20 and after the 4 year digits a dash is found
if (quotedString.length() == 20 && quotedString.charAt(4) == DATE_DATE_FIELD_DELIMITER) {
try {
result = new NSDate(quotedString);
} catch (Exception ex) {
//not a date? --> return string
result = new NSString(quotedString);
}
} else {
result = new NSString(quotedString);
}
break;
}
default: {
//0-9
if (this.data[this.index] >= '0' && this.data[this.index] <= '9') {
//could be a date or just a string
result = this.parseDateString();
} else {
//non-numerical -> string or boolean
result = new NSString(this.parseString());
}
break;
}
}
if (result != null) {
result.setLocationInformation(loc);
}
return result;
}
/**
* Parses an array from the current parsing position. The prerequisite for calling this method is,
* that an array begin token has been read.
*
* @return The array found at the parsing position.
*/
private NSArray parseArray() throws ParseException {
//Skip begin token
this.skip();
this.skipWhitespacesAndComments();
List objects = new LinkedList<>();
while (!this.accept(ARRAY_END_TOKEN)) {
objects.add(this.parseObject());
this.skipWhitespacesAndComments();
if (this.accept(ARRAY_ITEM_DELIMITER_TOKEN)) {
this.skip();
} else {
break; //must have reached end of array
}
this.skipWhitespacesAndComments();
}
//parse end token
this.read(ARRAY_END_TOKEN);
return new NSArray(objects.toArray(new NSObject[0]));
}
/**
* Parses a dictionary from the current parsing position. The prerequisite for calling this method
* is, that a dictionary begin token has been read.
*
* @return The dictionary found at the parsing position.
*/
private NSDictionary parseDictionary() throws ParseException {
//Skip begin token
this.skip();
this.skipWhitespacesAndComments();
NSDictionary dict = new NSDictionary();
while (!this.accept(DICTIONARY_END_TOKEN)) {
//Parse key
String keyString;
if (this.accept(QUOTEDSTRING_BEGIN_TOKEN)) {
keyString = this.parseQuotedString();
} else {
keyString = this.parseString();
}
this.skipWhitespacesAndComments();
//Parse assign token
this.read(DICTIONARY_ASSIGN_TOKEN);
this.skipWhitespacesAndComments();
NSObject object = this.parseObject();
dict.put(keyString, object);
this.skipWhitespacesAndComments();
this.read(DICTIONARY_ITEM_DELIMITER_TOKEN);
this.skipWhitespacesAndComments();
}
//skip end token
this.skip();
return dict;
}
/**
* Parses a data object from the current parsing position. This can either be a NSData object or a
* GnuStep NSNumber or NSDate. The prerequisite for calling this method is, that a data begin
* token has been read.
*
* @return The data object found at the parsing position.
*/
private NSObject parseData() throws ParseException {
int dataStartIndex = this.index;
NSObject obj = null;
//Skip begin token
this.skip();
if (this.accept(DATA_GSOBJECT_BEGIN_TOKEN)) {
this.skip();
this.expect(
DATA_GSBOOL_BEGIN_TOKEN,
DATA_GSDATE_BEGIN_TOKEN,
DATA_GSINT_BEGIN_TOKEN,
DATA_GSREAL_BEGIN_TOKEN);
if (this.accept(DATA_GSBOOL_BEGIN_TOKEN)) {
//Boolean
this.skip();
this.expect(DATA_GSBOOL_TRUE_TOKEN, DATA_GSBOOL_FALSE_TOKEN);
if (this.accept(DATA_GSBOOL_TRUE_TOKEN)) {
obj = new NSNumber(true);
} else {
obj = new NSNumber(false);
}
//Skip the parsed boolean token
this.skip();
} else if (this.accept(DATA_GSDATE_BEGIN_TOKEN)) {
//Date
this.skip();
String dateString = this.readInputUntil(DATA_END_TOKEN);
obj = new NSDate(dateString);
} else if (this.accept(DATA_GSINT_BEGIN_TOKEN, DATA_GSREAL_BEGIN_TOKEN)) {
//Number
this.skip();
String numberString = this.readInputUntil(DATA_END_TOKEN);
try {
obj = new NSNumber(numberString);
} catch (IllegalArgumentException ex) {
throw this.createParseException(
"The NSNumber object has an invalid format.", dataStartIndex);
}
}
// parse data end token
this.read(DATA_END_TOKEN);
} else if (this.accept(DATA_BASE64_BEGIN_TOKEN)) {
// skip DATA_BASE64_BEGIN_TOKEN token
this.skip();
String dataString = this.readInputUntil(DATA_BASE64_END_TOKEN);
try {
obj = new NSData(dataString);
} catch (IOException e) {
throw this.createParseException(
"The NSData object could be parsed.", dataStartIndex);
}
// skip DATA_BASE64_END_TOKEN token
this.skip();
// parse data end token
this.read(DATA_END_TOKEN);
} else {
String dataString = this.readInputUntil(DATA_END_TOKEN);
dataString = dataString.replaceAll("\\s+", "");
int numBytes = dataString.length() / 2;
byte[] bytes = new byte[numBytes];
int nibble1, nibble2;
for (int bi = 0, ci = 0; bi < bytes.length; bi++, ci += 2) {
nibble1 = Character.digit(dataString.charAt(ci), 16);
nibble2 = Character.digit(dataString.charAt(ci + 1), 16);
if (nibble1 == -1 || nibble2 == -1) {
throw this.createParseException(
"The NSData object contains non-hexadecimal characters.", dataStartIndex);
}
bytes[bi] = (byte) (nibble1 << 4 | nibble2);
}
obj = new NSData(bytes);
// skip DATA_END_TOKEN
this.skip();
}
return obj;
}
/**
* Attempts to parse a plain string as a date if possible.
*
* @return An NSDate if the string represents such an object. Otherwise, an NSString is returned.
*/
private NSObject parseDateString() {
String numericalString = this.parseString();
if (numericalString.length() > 4 && numericalString.charAt(4) == DATE_DATE_FIELD_DELIMITER) {
try {
return new NSDate(numericalString);
} catch (Exception ex) {
//An exception occurs if the string is not actually a date but just a string
}
}
return new NSString(numericalString);
}
/**
* Parses a plain string from the current parsing position. The string is made up of all
* characters to the next whitespace, delimiter token or assignment token.
*
* @return The string found at the current parsing position.
*/
private String parseString() {
return this.readInputUntil(WHITESPACE_SPACE, WHITESPACE_TAB, WHITESPACE_NEWLINE,
WHITESPACE_CARRIAGE_RETURN,
ARRAY_ITEM_DELIMITER_TOKEN, DICTIONARY_ITEM_DELIMITER_TOKEN, DICTIONARY_ASSIGN_TOKEN,
ARRAY_END_TOKEN);
}
/**
* Parses a quoted string from the current parsing position. The prerequisite for calling this
* method is, that a quoted string begin token has been read.
*
* @return The quoted string found at the parsing method with all special characters unescaped.
* @throws ParseException If an error occurred during parsing.
*/
private String parseQuotedString() throws ParseException {
int startIndex = this.index;
//Skip begin token
this.skip();
StringBuilder stringBuilder = new StringBuilder();
boolean unescapedBackslash = true;
EscapeSequenceHandler escapeSequenceHandler = null;
while (this.data[this.index] != QUOTEDSTRING_END_TOKEN
|| escapeSequenceHandler != null) {
char c = this.data[this.index];
if (escapeSequenceHandler != null) {
if (escapeSequenceHandler.handleNextChar(c)) {
escapeSequenceHandler = null;
}
} else if (c == QUOTEDSTRING_ESCAPE_TOKEN) {
escapeSequenceHandler = new EscapeSequenceHandler(stringBuilder);
} else {
stringBuilder.append(c);
}
this.trackLineBreak();
this.skip();
}
if (escapeSequenceHandler != null) {
escapeSequenceHandler.handleEndOfString();
}
//skip end token
this.skip();
return stringBuilder.toString();
}
private ParseException createParseException(String message) {
return this.createParseException(message, this.index);
}
private ParseException createParseException(String message, int index) {
return new ParseException(
message + " (" + this.lineNo + ":" + (index - this.lineBeginning) + ")",
index);
}
private class EscapeSequenceHandler {
private final int startIndex;
private final StringBuilder stringBuilder;
private int unicodeReferenceRadix = 0;
private StringBuilder unicodeReference;
public EscapeSequenceHandler(StringBuilder stringBuilder) {
this.startIndex = ASCIIPropertyListParser.this.index;
this.stringBuilder = stringBuilder;
}
public boolean handleNextChar(char c) throws ParseException {
switch (this.unicodeReferenceRadix) {
case 8:
return this.handleNextCharOfOctalEscapeSequence(c);
case 16:
return this.handleNextCharOfHexEscapeSequence(c);
default:
return this.handleFirstChar(c);
}
}
public void handleEndOfString() throws ParseException {
String sequence = new String(
ASCIIPropertyListParser.this.data,
this.startIndex,
ASCIIPropertyListParser.this.index - this.startIndex + 1);
throw ASCIIPropertyListParser.this.createParseException(
"The property list contains a string with an incomplete escape sequence: "
+ sequence,
this.startIndex);
}
private boolean handleFirstChar(char c) throws ParseException {
switch (c) {
case '\\':
case '"':
case '\'':
this.stringBuilder.append(c);
return true;
case 'b':
this.stringBuilder.append('\b');
return true;
case 'n':
this.stringBuilder.append('\n');
return true;
case 'r':
this.stringBuilder.append('\r');
return true;
case 't':
this.stringBuilder.append('\t');
return true;
case 'U':
case 'u':
this.unicodeReferenceRadix = 16;
this.unicodeReference = new StringBuilder(4);
return false;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
this.unicodeReferenceRadix = 8;
this.unicodeReference = new StringBuilder(3);
this.unicodeReference.append(c);
return false;
default:
throw ASCIIPropertyListParser.this.createParseException(
"The property list contains an invalid escape sequence: \\" + c,
this.startIndex);
}
}
private boolean handleNextCharOfHexEscapeSequence(char c) throws ParseException {
if (Character.digit(c, 16) == -1) {
String sequence = new String(
ASCIIPropertyListParser.this.data,
this.startIndex,
ASCIIPropertyListParser.this.index - this.startIndex + 1);
throw ASCIIPropertyListParser.this.createParseException(
"The property list contains a string with an invalid escape sequence: "
+ sequence,
this.startIndex);
}
this.unicodeReference.append(c);
if (this.unicodeReference.length() == 4) {
char escapedChar = (char) Integer.parseInt(this.unicodeReference.toString(),
16);
this.stringBuilder.append(escapedChar);
return true;
}
return false;
}
private boolean handleNextCharOfOctalEscapeSequence(char c) throws ParseException {
if (Character.digit(c, 8) == -1) {
String sequence = new String(
ASCIIPropertyListParser.this.data,
this.startIndex,
ASCIIPropertyListParser.this.index - this.startIndex + 1);
throw ASCIIPropertyListParser.this.createParseException(
"The property list contains a string with an invalid escape sequence: "
+ sequence,
this.startIndex);
}
this.unicodeReference.append(c);
if (this.unicodeReference.length() == 3) {
char escapedChar = (char) Integer.parseInt(this.unicodeReference.toString(),
8);
this.stringBuilder.append(escapedChar);
return true;
}
return false;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy