You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pivot.apache.org by gb...@apache.org on 2011/01/10 00:19:29 UTC

svn commit: r1057053 [3/12] - in /pivot/branches/3.x: ./ core/ core/src/ core/src/org/ core/src/org/apache/ core/src/org/apache/pivot/ core/src/org/apache/pivot/beans/ core/src/org/apache/pivot/bxml/ core/src/org/apache/pivot/csv/ core/src/org/apache/p...

Added: pivot/branches/3.x/core/src/org/apache/pivot/io/IOTask.java
URL: http://svn.apache.org/viewvc/pivot/branches/3.x/core/src/org/apache/pivot/io/IOTask.java?rev=1057053&view=auto
==============================================================================
--- pivot/branches/3.x/core/src/org/apache/pivot/io/IOTask.java (added)
+++ pivot/branches/3.x/core/src/org/apache/pivot/io/IOTask.java Sun Jan  9 23:19:19 2011
@@ -0,0 +1,207 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pivot.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.concurrent.ExecutorService;
+
+import org.apache.pivot.util.concurrent.AbortException;
+import org.apache.pivot.util.concurrent.Task;
+
+
+/**
+ * Abstract base class for input/output tasks.
+ */
+public abstract class IOTask<V> extends Task<V> {
+    /**
+     * Input stream that monitors the bytes that are read from it by
+     * incrementing the <tt>bytesReceived</tt> member variable.
+     */
+    protected class MonitoredInputStream extends InputStream {
+        private InputStream inputStream;
+
+        long mark = 0;
+
+        public MonitoredInputStream(InputStream inputStream) {
+            this.inputStream = inputStream;
+        }
+
+        @Override
+        public int read() throws IOException {
+            if (abort) {
+                throw new AbortException();
+            }
+
+            int result = inputStream.read();
+
+            if (result != -1) {
+                bytesReceived++;
+            }
+
+            return result;
+        }
+
+        @Override
+        public int read(byte b[]) throws IOException {
+            if (abort) {
+                throw new AbortException();
+            }
+
+            int count = inputStream.read(b);
+
+            if (count != -1) {
+                bytesReceived += count;
+            }
+
+            return count;
+        }
+
+        @Override
+        public int read(byte b[], int off, int len) throws IOException {
+            if (abort) {
+                throw new AbortException();
+            }
+
+            int count = inputStream.read(b, off, len);
+
+            if (count != -1) {
+                bytesReceived += count;
+            }
+
+            return count;
+        }
+
+        @Override
+        public long skip(long n) throws IOException {
+            if (abort) {
+                throw new AbortException();
+            }
+
+            long count = inputStream.skip(n);
+            bytesReceived += count;
+            return count;
+        }
+
+        @Override
+        public int available() throws IOException {
+            if (abort) {
+                throw new AbortException();
+            }
+
+            return inputStream.available();
+        }
+
+        @Override
+        public void close() throws IOException {
+            inputStream.close();
+        }
+
+        @Override
+        public void mark(int readLimit) {
+            if (abort) {
+                throw new AbortException();
+            }
+
+            inputStream.mark(readLimit);
+            mark = bytesReceived;
+        }
+
+        @Override
+        public void reset() throws IOException {
+            if (abort) {
+                throw new AbortException();
+            }
+
+            inputStream.reset();
+            bytesReceived = mark;
+        }
+
+        @Override
+        public boolean markSupported() {
+            return inputStream.markSupported();
+        }
+    }
+
+    /**
+     * Output stream that monitors the bytes that are written to it by
+     * incrementing the <tt>bytesSent</tt> member variable.
+     */
+    protected class MonitoredOutputStream extends OutputStream {
+        private OutputStream outputStream;
+
+        public MonitoredOutputStream(OutputStream outputStream) {
+            this.outputStream = outputStream;
+        }
+
+        @Override
+        public void close() throws IOException {
+            outputStream.close();
+        }
+
+        @Override
+        public void flush() throws IOException {
+            if (abort) {
+                throw new AbortException();
+            }
+
+            outputStream.flush();
+        }
+
+        @Override
+        public void write(byte[] b) throws IOException {
+            if (abort) {
+                throw new AbortException();
+            }
+
+            outputStream.write(b);
+            bytesSent += b.length;
+        }
+
+        @Override
+        public void write(byte[] b, int off, int len) throws IOException {
+            if (abort) {
+                throw new AbortException();
+            }
+
+            outputStream.write(b, off, len);
+            bytesSent += len;
+        }
+
+        @Override
+        public void write(int b) throws IOException {
+            if (abort) {
+                throw new AbortException();
+            }
+
+            outputStream.write(b);
+            bytesSent++;
+        }
+    }
+
+    protected volatile long bytesSent = 0;
+    protected volatile long bytesReceived = 0;
+
+    public IOTask() {
+        super();
+    }
+
+    public IOTask(ExecutorService executorService) {
+        super(executorService);
+    }
+}

Added: pivot/branches/3.x/core/src/org/apache/pivot/io/PropertiesSerializer.java
URL: http://svn.apache.org/viewvc/pivot/branches/3.x/core/src/org/apache/pivot/io/PropertiesSerializer.java?rev=1057053&view=auto
==============================================================================
--- pivot/branches/3.x/core/src/org/apache/pivot/io/PropertiesSerializer.java (added)
+++ pivot/branches/3.x/core/src/org/apache/pivot/io/PropertiesSerializer.java Sun Jan  9 23:19:19 2011
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pivot.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * Implementation of the {@link Serializer} interface that reads data from
+ * and writes data to the Java properties file format.
+ *
+ */
+public class PropertiesSerializer implements Serializer<Map<?, ?>> {
+    public static final String PROPERTIES_EXTENSION = "properties";
+    public static final String MIME_TYPE = "text/plain";
+
+    /**
+     * Reads data from a properties stream.
+     *
+     * @param inputStream
+     * The input stream from which data will be read.
+     *
+     * @return
+     * An instance of {@link Map} containing the data read from the properties
+     * file. Both keys and values are strings.
+     */
+    @Override
+    public Map<?, ?> readObject(InputStream inputStream) throws IOException,
+        SerializationException {
+        if (inputStream == null) {
+            throw new IllegalArgumentException("inputStream is null.");
+        }
+
+        Properties properties = new Properties();
+        properties.load(inputStream);
+
+        return properties;
+    }
+
+    /**
+     * Writes data to a properties stream.
+     *
+     * @param object
+     * An instance of {@link Map} containing the data to be written to the
+     * properties file. Keys must be strings, and values will be converted to
+     * strings.
+     *
+     * @param outputStream
+     * The output stream to which data will be written.
+     */
+    @SuppressWarnings("unchecked")
+    @Override
+    public void writeObject(Map<?, ?> object, OutputStream outputStream) throws IOException,
+        SerializationException {
+        if (object == null) {
+            throw new IllegalArgumentException("object is null.");
+        }
+
+        if (outputStream == null) {
+            throw new IllegalArgumentException("outputStream is null.");
+        }
+
+        Map<Object, Object> map = (Map<Object, Object>)object;
+
+        Properties properties = new Properties();
+
+        for (Map.Entry<Object, Object> entry : map.entrySet()) {
+            Object key = entry.getKey();
+            Object value = entry.getValue();
+
+            if (value != null) {
+                value = value.toString();
+            }
+
+            properties.put(key, value);
+        }
+
+        properties.store(outputStream, null);
+    }
+
+    @Override
+    public String getMIMEType(Map<?, ?> object) {
+        return MIME_TYPE;
+    }
+}

Added: pivot/branches/3.x/core/src/org/apache/pivot/io/SerializationException.java
URL: http://svn.apache.org/viewvc/pivot/branches/3.x/core/src/org/apache/pivot/io/SerializationException.java?rev=1057053&view=auto
==============================================================================
--- pivot/branches/3.x/core/src/org/apache/pivot/io/SerializationException.java (added)
+++ pivot/branches/3.x/core/src/org/apache/pivot/io/SerializationException.java Sun Jan  9 23:19:19 2011
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pivot.io;
+
+/**
+ * Thrown when an error is encountered during serialization.
+ */
+public class SerializationException extends Exception {
+    private static final long serialVersionUID = 0;
+
+    public SerializationException() {
+        super();
+    }
+
+    public SerializationException(String message) {
+        super(message);
+    }
+
+    public SerializationException(Throwable cause) {
+        super(cause);
+    }
+
+    public SerializationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

Added: pivot/branches/3.x/core/src/org/apache/pivot/io/Serializer.java
URL: http://svn.apache.org/viewvc/pivot/branches/3.x/core/src/org/apache/pivot/io/Serializer.java?rev=1057053&view=auto
==============================================================================
--- pivot/branches/3.x/core/src/org/apache/pivot/io/Serializer.java (added)
+++ pivot/branches/3.x/core/src/org/apache/pivot/io/Serializer.java Sun Jan  9 23:19:19 2011
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pivot.io;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.IOException;
+
+/**
+ * Defines an interface for writing objects to and reading objects from a data
+ * stream.
+ *
+ * @param <T>
+ * The type of data being read and written.
+ */
+public interface Serializer<T> {
+    /**
+     * Reads an object from an input stream.
+     *
+     * @param inputStream
+     * The data stream from which the object will be read.
+     *
+     * @return
+     * The deserialized object.
+     */
+    public T readObject(InputStream inputStream) throws IOException, SerializationException;
+
+    /**
+     * Writes an object to an output stream.
+     *
+     * @param object
+     * The object to serialize.
+     *
+     * @param outputStream
+     * The data stream to which the object will be written.
+     */
+    public void writeObject(T object, OutputStream outputStream) throws IOException, SerializationException;
+
+    /**
+     * Returns the MIME type of the data read and written by this serializer.
+     *
+     * @param object
+     * If provided, allows the serializer to attach parameters to the returned
+     * MIME type containing more detailed information about the data. If
+     * <tt>null</tt>, the base MIME type is returned.
+     */
+    public String getMIMEType(T object);
+}

Added: pivot/branches/3.x/core/src/org/apache/pivot/io/StringSerializer.java
URL: http://svn.apache.org/viewvc/pivot/branches/3.x/core/src/org/apache/pivot/io/StringSerializer.java?rev=1057053&view=auto
==============================================================================
--- pivot/branches/3.x/core/src/org/apache/pivot/io/StringSerializer.java (added)
+++ pivot/branches/3.x/core/src/org/apache/pivot/io/StringSerializer.java Sun Jan  9 23:19:19 2011
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pivot.io;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+
+/**
+ * Implementation of the {@link Serializer} interface that reads data from
+ * and writes data to Java Strings.
+ */
+public class StringSerializer implements Serializer<String> {
+    private final Charset charset;
+
+    public static final String DEFAULT_CHARSET_NAME = "UTF-8";
+    public static final String TEXT_EXTENSION = "txt";
+    public static final String MIME_TYPE = "text/plain";
+    public static final int BUFFER_SIZE = 2048;
+
+    public StringSerializer() {
+        this(Charset.forName(DEFAULT_CHARSET_NAME));
+    }
+
+    public StringSerializer(Charset charset) {
+        if (charset == null) {
+            throw new IllegalArgumentException("charset is null.");
+        }
+
+        this.charset = charset;
+    }
+
+    public Charset getCharset() {
+        return charset;
+    }
+
+    /**
+     * Reads plain text data from an input stream.
+     *
+     * @param inputStream
+     * The input stream from which data will be read.
+     *
+     * @return
+     * An instance of {@link String} containing the text read from the input stream.
+     */
+    @Override
+    public String readObject(InputStream inputStream) throws IOException, SerializationException {
+        if (inputStream == null) {
+            throw new IllegalArgumentException("inputStream is null.");
+        }
+
+        String result = null;
+
+        try {
+            BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
+            ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
+
+            byte[] buffer = new byte[BUFFER_SIZE];
+            int read;
+            while ((read = bufferedInputStream.read(buffer)) != -1) {
+                byteOutputStream.write(buffer, 0, read);
+            }
+
+            byteOutputStream.flush();
+
+            result = new String(byteOutputStream.toByteArray(), charset);
+        } catch (IOException exception) {
+            throw new SerializationException(exception);
+        }
+
+        return result;
+    }
+
+    /**
+     * Writes plain text data to an output stream.
+     *
+     * @param text
+     * The text to be written to the output stream.
+     *
+     * @param outputStream
+     * The output stream to which data will be written.
+     */
+    @Override
+    public void writeObject(String text, OutputStream outputStream)
+        throws IOException, SerializationException {
+        if (text == null) {
+            throw new IllegalArgumentException("text is null.");
+        }
+
+        if (outputStream == null) {
+            throw new IllegalArgumentException("outputStream is null.");
+        }
+
+        try {
+            BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);
+            bufferedOutputStream.write(text.getBytes());
+            bufferedOutputStream.flush();
+        } catch (IOException exception) {
+            throw new SerializationException(exception);
+        }
+    }
+
+    @Override
+    public String getMIMEType(String object) {
+        return MIME_TYPE;
+    }
+}

Added: pivot/branches/3.x/core/src/org/apache/pivot/json/JSONSerializer.java
URL: http://svn.apache.org/viewvc/pivot/branches/3.x/core/src/org/apache/pivot/json/JSONSerializer.java?rev=1057053&view=auto
==============================================================================
--- pivot/branches/3.x/core/src/org/apache/pivot/json/JSONSerializer.java (added)
+++ pivot/branches/3.x/core/src/org/apache/pivot/json/JSONSerializer.java Sun Jan  9 23:19:19 2011
@@ -0,0 +1,1204 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pivot.json;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.LineNumberReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.pivot.beans.BeanAdapter;
+import org.apache.pivot.io.SerializationException;
+import org.apache.pivot.io.Serializer;
+import org.apache.pivot.util.ListenerList;
+
+/**
+ * Implementation of the {@link Serializer} interface that reads data from
+ * and writes data to a JavaScript Object Notation (JSON) file.
+ */
+public class JSONSerializer implements Serializer<Object> {
+    private static class JSONSerializerListenerList
+        extends ListenerList<JSONSerializerListener>
+        implements JSONSerializerListener {
+        @Override
+        public void beginMap(JSONSerializer jsonSerializer, Map<String, ?> value) {
+            for (JSONSerializerListener listener : listeners()) {
+                listener.beginMap(jsonSerializer, value);
+            }
+        }
+
+        @Override
+        public void endMap(JSONSerializer jsonSerializer) {
+            for (JSONSerializerListener listener : listeners()) {
+                listener.endMap(jsonSerializer);
+            }
+        }
+
+        @Override
+        public void readKey(JSONSerializer jsonSerializer, String key) {
+            for (JSONSerializerListener listener : listeners()) {
+                listener.readKey(jsonSerializer, key);
+            }
+        }
+
+        @Override
+        public void beginList(JSONSerializer jsonSerializer, List<?> value) {
+            for (JSONSerializerListener listener : listeners()) {
+                listener.beginList(jsonSerializer, value);
+            }
+        }
+
+        @Override
+        public void endList(JSONSerializer jsonSerializer) {
+            for (JSONSerializerListener listener : listeners()) {
+                listener.endList(jsonSerializer);
+            }
+        }
+
+        @Override
+        public void readString(JSONSerializer jsonSerializer, String value) {
+            for (JSONSerializerListener listener : listeners()) {
+                listener.readString(jsonSerializer, value);
+            }
+        }
+
+        @Override
+        public void readNumber(JSONSerializer jsonSerializer, Number value) {
+            for (JSONSerializerListener listener : listeners()) {
+                listener.readNumber(jsonSerializer, value);
+            }
+        }
+
+        @Override
+        public void readBoolean(JSONSerializer jsonSerializer, Boolean value) {
+            for (JSONSerializerListener listener : listeners()) {
+                listener.readBoolean(jsonSerializer, value);
+            }
+        }
+
+        @Override
+        public void readNull(JSONSerializer jsonSerializer) {
+            for (JSONSerializerListener listener : listeners()) {
+                listener.readNull(jsonSerializer);
+            }
+        }
+    }
+
+    private Charset charset;
+    private Type type;
+
+    private boolean alwaysDelimitMapKeys = false;
+
+    private int c = -1;
+
+    private JSONSerializerListenerList jsonSerializerListeners = null;
+
+    public static final String DEFAULT_CHARSET_NAME = "UTF-8";
+    public static final Type DEFAULT_TYPE = Object.class;
+
+    public static final String JSON_EXTENSION = "json";
+    public static final String MIME_TYPE = "application/json";
+    public static final int BUFFER_SIZE = 2048;
+
+    public JSONSerializer() {
+        this(Charset.forName(DEFAULT_CHARSET_NAME), DEFAULT_TYPE);
+    }
+
+    public JSONSerializer(Charset charset) {
+        this(charset, DEFAULT_TYPE);
+    }
+
+    public JSONSerializer(Type type) {
+        this(Charset.forName(DEFAULT_CHARSET_NAME), type);
+    }
+
+    public JSONSerializer(Charset charset, Type type) {
+        if (charset == null) {
+            throw new IllegalArgumentException("charset is null.");
+        }
+
+        if (type == null) {
+            throw new IllegalArgumentException("type is null.");
+        }
+
+        this.charset = charset;
+        this.type = type;
+    }
+
+    /**
+     * Returns the character set used to encode/decode the JSON data.
+     */
+    public Charset getCharset() {
+        return charset;
+    }
+
+    /**
+     * Returns the type of the object that will be returned by {@link #readObject(Reader)}.
+     */
+    public Type getType() {
+        return type;
+    }
+
+    /**
+     * Returns a flag indicating whether or not map keys will always be
+     * quote-delimited.
+     */
+    public boolean getAlwaysDelimitMapKeys() {
+        return alwaysDelimitMapKeys;
+    }
+
+    /**
+     * Sets a flag indicating that map keys should always be quote-delimited.
+     *
+     * @param alwaysDelimitMapKeys
+     * <tt>true</tt> to bound map keys in double quotes; <tt>false</tt> to
+     * only quote-delimit keys as necessary.
+     */
+    public void setAlwaysDelimitMapKeys(boolean alwaysDelimitMapKeys) {
+        this.alwaysDelimitMapKeys = alwaysDelimitMapKeys;
+    }
+
+    /**
+     * Reads data from a JSON stream.
+     *
+     * @param inputStream
+     * The input stream from which data will be read.
+     *
+     * @see #readObject(Reader)
+     */
+    @Override
+    public Object readObject(InputStream inputStream)
+        throws IOException, SerializationException {
+        if (inputStream == null) {
+            throw new IllegalArgumentException("inputStream is null.");
+        }
+
+        Reader reader = new BufferedReader(new InputStreamReader(inputStream, charset), BUFFER_SIZE);
+        return readObject(reader);
+    }
+
+    /**
+     * Reads data from a JSON stream.
+     *
+     * @param reader
+     * The reader from which data will be read.
+     *
+     * @return
+     * One of the following types, depending on the content of the stream
+     * and the value of {@link #getType()}:
+     *
+     * <ul>
+     * <li>pivot.collections.Dictionary</li>
+     * <li>pivot.collections.Sequence</li>
+     * <li>java.lang.String</li>
+     * <li>java.lang.Number</li>
+     * <li>java.lang.Boolean</li>
+     * <li><tt>null</tt></li>
+     * <li>A JavaBean object</li>
+     * </ul>
+     */
+    public Object readObject(Reader reader)
+        throws IOException, SerializationException {
+        if (reader == null) {
+            throw new IllegalArgumentException("reader is null.");
+        }
+
+        // Move to the first character
+        LineNumberReader lineNumberReader = new LineNumberReader(reader);
+        c = lineNumberReader.read();
+
+        // Ignore BOM (if present)
+        if (c == 0xFEFF) {
+            c = lineNumberReader.read();
+        }
+
+        // Read the root value
+        Object object;
+        try {
+            object = readValue(lineNumberReader, type);
+        } catch (SerializationException exception) {
+            System.err.println("An error occurred while processing input at line number "
+                + (lineNumberReader.getLineNumber() + 1));
+
+            throw exception;
+        }
+
+        return object;
+    }
+
+    private Object readValue(Reader reader, Type type)
+        throws IOException, SerializationException {
+        Object object = null;
+
+        skipWhitespaceAndComments(reader);
+
+        if (c == -1) {
+            throw new SerializationException("Unexpected end of input stream.");
+        }
+
+        if (c == 'n') {
+            object = readNullValue(reader);
+        } else if (c == '"' || c == '\'') {
+            object = readStringValue(reader, type);
+        } else if (c == '+' || c == '-' || Character.isDigit(c)) {
+            object = readNumberValue(reader, type);
+        } else if (c == 't' || c == 'f') {
+            object = readBooleanValue(reader, type);
+        } else if (c == '[') {
+            object = readListValue(reader, type);
+        } else if (c == '{') {
+            object = readMapValue(reader, type);
+        } else {
+            throw new SerializationException("Unexpected character in input stream.");
+        }
+
+        return object;
+    }
+
+    private void skipWhitespaceAndComments(Reader reader)
+        throws IOException, SerializationException {
+        while (c != -1
+            && (Character.isWhitespace(c)
+                || c == '/')) {
+            boolean comment = (c == '/');
+
+            // Read the next character
+            c = reader.read();
+
+            if (comment) {
+                if (c == '/') {
+                    // Single-line comment
+                    while (c != -1
+                        && c != '\n'
+                        && c != '\r') {
+                        c = reader.read();
+                    }
+                } else if (c == '*') {
+                    // Multi-line comment
+                    boolean closed = false;
+
+                    while (c != -1
+                        && !closed) {
+                        c = reader.read();
+
+                        if (c == '*') {
+                            c = reader.read();
+                            closed = (c == '/');
+                        }
+                    }
+
+                    if (!closed) {
+                        throw new SerializationException("Unexpected end of input stream.");
+                    }
+
+                    if (c != -1) {
+                        c = reader.read();
+                    }
+                } else {
+                    throw new SerializationException("Unexpected character in input stream.");
+                }
+            }
+        }
+    }
+
+    private Object readNullValue(Reader reader)
+        throws IOException, SerializationException {
+        String nullString = "null";
+
+        int n = nullString.length();
+        int i = 0;
+
+        while (c != -1 && i < n) {
+            if (nullString.charAt(i) != c) {
+                throw new SerializationException("Unexpected character in input stream.");
+            }
+
+            c = reader.read();
+            i++;
+        }
+
+        if (i < n) {
+            throw new SerializationException("Incomplete null value in input stream.");
+        }
+
+        // Notify the listeners
+        if (jsonSerializerListeners != null) {
+            jsonSerializerListeners.readNull(this);
+        }
+
+        return null;
+    }
+
+    private String readString(Reader reader)
+        throws IOException, SerializationException {
+        StringBuilder stringBuilder = new StringBuilder();
+
+        // Use the same delimiter to close the string
+        int t = c;
+
+        // Move to the next character after the delimiter
+        c = reader.read();
+
+        while (c != -1 && c != t) {
+            if (!Character.isISOControl(c)) {
+                if (c == '\\') {
+                    c = reader.read();
+
+                    if (c == 'b') {
+                        c = '\b';
+                    } else if (c == 'f') {
+                        c = '\f';
+                    } else if (c == 'n') {
+                        c = '\n';
+                    } else if (c == 'r') {
+                        c = '\r';
+                    } else if (c == 't') {
+                        c = '\t';
+                    } else if (c == 'u') {
+                        StringBuilder unicodeBuilder = new StringBuilder();
+                        while (unicodeBuilder.length() < 4) {
+                            c = reader.read();
+                            unicodeBuilder.append((char)c);
+                        }
+
+                        String unicode = unicodeBuilder.toString();
+                        c = (char)Integer.parseInt(unicode, 16);
+                    } else {
+                        if (!(c == '\\'
+                            || c == '/'
+                            || c == '\"'
+                            || c == '\''
+                            || c == t)) {
+                            throw new SerializationException("Unsupported escape sequence in input stream.");
+                        }
+                    }
+                }
+
+                stringBuilder.append((char)c);
+            }
+
+            c = reader.read();
+        }
+
+        if (c != t) {
+            throw new SerializationException("Unterminated string in input stream.");
+        }
+
+        // Move to the next character after the delimiter
+        c = reader.read();
+
+        return stringBuilder.toString();
+    }
+
+    private Object readStringValue(Reader reader, Type type)
+        throws IOException, SerializationException {
+        if (!(type instanceof Class<?>)) {
+            throw new SerializationException("Cannot convert string to " + type + ".");
+        }
+
+        String string = readString(reader);
+
+        // Notify the listeners
+        if (jsonSerializerListeners != null) {
+            jsonSerializerListeners.readString(this, string);
+        }
+
+        return BeanAdapter.coerce(string, (Class<?>)type);
+    }
+
+    private Object readNumberValue(Reader reader, Type type)
+        throws IOException, SerializationException {
+        if (!(type instanceof Class<?>)) {
+            throw new SerializationException("Cannot convert number to " + type + ".");
+        }
+
+        Number number = null;
+
+        StringBuilder stringBuilder = new StringBuilder();
+        boolean negative = false;
+        boolean integer = true;
+
+        if (c == '+' || c == '-') {
+            negative = (c == '-');
+            c = reader.read();
+        }
+
+        while (c != -1 && (Character.isDigit(c) || c == '.'
+            || c == 'e' || c == 'E' || c == '-')) {
+            stringBuilder.append((char)c);
+            integer &= !(c == '.');
+            c = reader.read();
+        }
+
+        if (integer) {
+            long value = Long.parseLong(stringBuilder.toString()) * (negative ? -1 : 1);
+
+            if (value > Integer.MAX_VALUE
+                || value < Integer.MIN_VALUE) {
+                number = value;
+            } else {
+                number = (int)value;
+            }
+        } else {
+            number = Double.parseDouble(stringBuilder.toString()) * (negative ? -1.0d : 1.0d);
+        }
+
+        // Notify the listeners
+        if (jsonSerializerListeners != null) {
+            jsonSerializerListeners.readNumber(this, number);
+        }
+
+        return BeanAdapter.coerce(number, (Class<?>)type);
+    }
+
+    private Object readBooleanValue(Reader reader, Type type)
+        throws IOException, SerializationException {
+        if (!(type instanceof Class<?>)) {
+            throw new SerializationException("Cannot convert number to " + type + ".");
+        }
+
+        String text = (c == 't') ? "true" : "false";
+        int n = text.length();
+        int i = 0;
+
+        while (c != -1 && i < n) {
+            if (text.charAt(i) != c) {
+                throw new SerializationException("Unexpected character in input stream.");
+            }
+
+            c = reader.read();
+            i++;
+        }
+
+        if (i < n) {
+            throw new SerializationException("Incomplete boolean value in input stream.");
+        }
+
+        // Get the boolean value
+        Boolean value = Boolean.parseBoolean(text);
+
+        // Notify the listeners
+        if (jsonSerializerListeners != null) {
+            jsonSerializerListeners.readBoolean(this, value);
+        }
+
+        return BeanAdapter.coerce(value, (Class<?>)type);
+    }
+
+    @SuppressWarnings("unchecked")
+    private Object readListValue(Reader reader, Type type)
+        throws IOException, SerializationException {
+        List<Object> list = null;
+        Type itemType = null;
+
+        if (type == Object.class) {
+            // Return the default list and item types
+            list = new ArrayList<Object>();
+            itemType = Object.class;
+        } else {
+            // Determine the item type from generic parameters
+            Type parentType = type;
+            while (parentType != null) {
+                if (parentType instanceof ParameterizedType) {
+                    ParameterizedType parameterizedType = (ParameterizedType)parentType;
+                    Class<?> rawType = (Class<?>)parameterizedType.getRawType();
+
+                    if (List.class.isAssignableFrom(rawType)) {
+                        itemType = parameterizedType.getActualTypeArguments()[0];
+                    }
+
+                    break;
+                } else {
+                    Class<?> classType = (Class<?>)parentType;
+                    Type[] genericInterfaces = classType.getGenericInterfaces();
+
+                    for (int i = 0; i < genericInterfaces.length; i++) {
+                        Type genericInterface = genericInterfaces[i];
+
+                        if (genericInterface instanceof ParameterizedType) {
+                            ParameterizedType parameterizedType = (ParameterizedType)genericInterface;
+                            Class<?> interfaceType = (Class<?>)parameterizedType.getRawType();
+
+                            if (List.class.isAssignableFrom(interfaceType)) {
+                                itemType = parameterizedType.getActualTypeArguments()[0];
+
+                                if (itemType instanceof TypeVariable<?>) {
+                                    itemType = Object.class;
+                                }
+
+                                break;
+                            }
+                        }
+                    }
+
+                    if (itemType != null) {
+                        break;
+                    }
+
+                    parentType = classType.getGenericSuperclass();
+                }
+            }
+
+            if (itemType == null) {
+                throw new SerializationException("Could not determine list item type.");
+            }
+
+            // Instantiate the list type
+            Class<?> listType;
+            if (type instanceof ParameterizedType) {
+                ParameterizedType parameterizedType = (ParameterizedType)type;
+                listType = (Class<?>)parameterizedType.getRawType();
+            } else {
+                listType = (Class<?>)type;
+            }
+
+            try {
+                list = (List<Object>)listType.newInstance();
+            } catch (InstantiationException exception) {
+                throw new RuntimeException(exception);
+            } catch (IllegalAccessException exception) {
+                throw new RuntimeException(exception);
+            }
+        }
+
+        // Notify the listeners
+        if (jsonSerializerListeners != null) {
+            jsonSerializerListeners.beginList(this, list);
+        }
+
+        // Move to the next character after '['
+        c = reader.read();
+        skipWhitespaceAndComments(reader);
+
+        while (c != -1 && c != ']') {
+            list.add(readValue(reader, itemType));
+            skipWhitespaceAndComments(reader);
+
+            if (c == ',') {
+                c = reader.read();
+                skipWhitespaceAndComments(reader);
+            } else if (c == -1) {
+                throw new SerializationException("Unexpected end of input stream.");
+            } else {
+                if (c != ']') {
+                    throw new SerializationException("Unexpected character in input stream.");
+                }
+            }
+        }
+
+        // Move to the next character after ']'
+        c = reader.read();
+
+        // Notify the listeners
+        if (jsonSerializerListeners != null) {
+            jsonSerializerListeners.endList(this);
+        }
+
+        return list;
+    }
+
+    @SuppressWarnings("unchecked")
+    private Object readMapValue(Reader reader, Type type)
+        throws IOException, SerializationException {
+        Map<String, Object> map = null;
+        Type valueType = null;
+
+        if (type == Object.class) {
+            // Return the default map and value types
+            map = new HashMap<String, Object>();
+            valueType = Object.class;
+        } else {
+            // Determine the value type from generic parameters
+            Type parentType = type;
+            while (parentType != null) {
+                if (parentType instanceof ParameterizedType) {
+                    ParameterizedType parameterizedType = (ParameterizedType)parentType;
+                    Class<?> rawType = (Class<?>)parameterizedType.getRawType();
+
+                    if (Map.class.isAssignableFrom(rawType)) {
+                        valueType = parameterizedType.getActualTypeArguments()[1];
+                    }
+
+                    break;
+                } else {
+                    Class<?> classType = (Class<?>)parentType;
+                    Type[] genericInterfaces = classType.getGenericInterfaces();
+
+                    for (int i = 0; i < genericInterfaces.length; i++) {
+                        Type genericInterface = genericInterfaces[i];
+
+                        if (genericInterface instanceof ParameterizedType) {
+                            ParameterizedType parameterizedType = (ParameterizedType)genericInterface;
+                            Class<?> interfaceType = (Class<?>)parameterizedType.getRawType();
+
+                            if (Map.class.isAssignableFrom(interfaceType)) {
+                                valueType = parameterizedType.getActualTypeArguments()[1];
+
+                                if (valueType instanceof TypeVariable<?>) {
+                                    valueType = Object.class;
+                                }
+
+                                break;
+                            }
+                        }
+                    }
+
+                    if (valueType != null) {
+                        break;
+                    }
+
+                    parentType = classType.getGenericSuperclass();
+                }
+            }
+
+            // Instantiate the map or bean type
+            if (valueType == null) {
+                Class<?> beanType = (Class<?>)type;
+
+                try {
+                    map = new BeanAdapter(beanType.newInstance());
+                } catch (InstantiationException exception) {
+                    throw new RuntimeException(exception);
+                } catch (IllegalAccessException exception) {
+                    throw new RuntimeException(exception);
+                }
+            } else {
+                Class<?> mapType;
+                if (type instanceof ParameterizedType) {
+                    ParameterizedType parameterizedType = (ParameterizedType)type;
+                    mapType = (Class<?>)parameterizedType.getRawType();
+                } else {
+                    mapType = (Class<?>)type;
+                }
+
+                try {
+                    map = (Map<String, Object>)mapType.newInstance();
+                } catch (InstantiationException exception) {
+                    throw new RuntimeException(exception);
+                } catch (IllegalAccessException exception) {
+                    throw new RuntimeException(exception);
+                }
+            }
+        }
+
+        // Notify the listeners
+        if (jsonSerializerListeners != null) {
+            jsonSerializerListeners.beginMap(this, map);
+        }
+
+        // Move to the next character after '{'
+        c = reader.read();
+        skipWhitespaceAndComments(reader);
+
+        while (c != -1 && c != '}') {
+            String key = null;
+
+            if (c == '"' || c == '\'') {
+                // The key is a delimited string
+                key = readString(reader);
+            } else {
+                // The key is an undelimited string; it must adhere to Java
+                // identifier syntax
+                StringBuilder keyBuilder = new StringBuilder();
+
+                if (!Character.isJavaIdentifierStart(c)) {
+                    throw new SerializationException("Illegal identifier start character.");
+                }
+
+                while (c != -1
+                    && c != ':' && !Character.isWhitespace(c)) {
+                    if (!Character.isJavaIdentifierPart(c)) {
+                        throw new SerializationException("Illegal identifier character.");
+                    }
+
+                    keyBuilder.append((char)c);
+                    c = reader.read();
+                }
+
+                if (c == -1) {
+                    throw new SerializationException("Unexpected end of input stream.");
+                }
+
+                key = keyBuilder.toString();
+            }
+
+            if (key == null
+                || key.length() == 0) {
+                throw new SerializationException("\"" + key + "\" is not a valid key.");
+            }
+
+            // Notify listeners
+            if (jsonSerializerListeners != null) {
+                jsonSerializerListeners.readKey(this, key);
+            }
+
+            skipWhitespaceAndComments(reader);
+
+            if (c != ':') {
+                throw new SerializationException("Unexpected character in input stream.");
+            }
+
+            // Move to the first character after ':'
+            c = reader.read();
+
+            if (valueType == null) {
+                // The map is a bean instance; get the generic type of the property
+                Type genericValueType = ((BeanAdapter)map).getGenericType(key);
+
+                if (genericValueType != null) {
+                    // Set the value in the bean
+                    map.put(key, readValue(reader, genericValueType));
+                } else {
+                    // The property does not exist; ignore this value
+                    readValue(reader, Object.class);
+                }
+            } else {
+                map.put(key, readValue(reader, valueType));
+            }
+
+            skipWhitespaceAndComments(reader);
+
+            if (c == ',') {
+                c = reader.read();
+                skipWhitespaceAndComments(reader);
+            } else if (c == -1) {
+                throw new SerializationException("Unexpected end of input stream.");
+            } else {
+                if (c != '}') {
+                    throw new SerializationException("Unexpected character in input stream.");
+                }
+            }
+        }
+
+        // Move to the first character after '}'
+        c = reader.read();
+
+        // Notify the listeners
+        if (jsonSerializerListeners != null) {
+            jsonSerializerListeners.endMap(this);
+        }
+
+        return (map instanceof BeanAdapter) ? ((BeanAdapter)map).getBean() : map;
+    }
+
+    /**
+     * Writes data to a JSON stream.
+     *
+     * @param object
+     *
+     * @param outputStream
+     * The output stream to which data will be written.
+     *
+     * @see #writeObject(Object, Writer)
+     */
+    @Override
+    public void writeObject(Object object, OutputStream outputStream)
+        throws IOException, SerializationException {
+        if (outputStream == null) {
+            throw new IllegalArgumentException("outputStream is null.");
+        }
+
+        Writer writer = new BufferedWriter(new OutputStreamWriter(outputStream, charset), BUFFER_SIZE);
+        writeObject(object, writer);
+    }
+
+    /**
+     * Writes data to a JSON stream.
+     *
+     * @param object
+     * The object to serialize. Must be one of the following types:
+     *
+     * <ul>
+     * <li>pivot.collections.Map</li>
+     * <li>pivot.collections.List</li>
+     * <li>java.lang.String</li>
+     * <li>java.lang.Number</li>
+     * <li>java.lang.Boolean</li>
+     * <li><tt>null</tt></li>
+     * </ul>
+     *
+     * @param writer
+     * The writer to which data will be written.
+     */
+    @SuppressWarnings("unchecked")
+    public void writeObject(Object object, Writer writer)
+        throws IOException, SerializationException {
+        if (writer == null) {
+            throw new IllegalArgumentException("writer is null.");
+        }
+
+        if (object == null) {
+            writer.append("null");
+        } else if (object instanceof String) {
+            String string = (String)object;
+            StringBuilder stringBuilder = new StringBuilder();
+
+            for (int i = 0, n = string.length(); i < n; i++) {
+                char c = string.charAt(i);
+
+                switch(c) {
+                    case '\t': {
+                        stringBuilder.append("\\t");
+                        break;
+                    }
+
+                    case '\n': {
+                        stringBuilder.append("\\n");
+                        break;
+                    }
+
+                    case '\\':
+                    case '\"':
+                    case '\'': {
+                        stringBuilder.append("\\" + c);
+                        break;
+                    }
+
+                    default: {
+                        if (charset.name().startsWith("UTF")
+                            || c <= 0xFF) {
+                            stringBuilder.append(c);
+                        } else {
+                            stringBuilder.append("\\u");
+                            stringBuilder.append(String.format("%04x", (short)c));
+                        }
+                    }
+                }
+
+            }
+
+            writer.append("\"" + stringBuilder.toString() + "\"");
+        } else if (object instanceof Number) {
+            Number number = (Number)object;
+
+            if (number instanceof Float) {
+                Float f = (Float)number;
+                if (f.isNaN()
+                    || f.isInfinite()) {
+                    throw new SerializationException(number + " is not a valid value.");
+                }
+            } else if (number instanceof Double) {
+                Double d = (Double)number;
+                if (d.isNaN()
+                    || d.isInfinite()) {
+                    throw new SerializationException(number + " is not a valid value.");
+                }
+            }
+
+            writer.append(number.toString());
+        } else if (object instanceof Boolean) {
+            writer.append(object.toString());
+        } else if (object instanceof List<?>) {
+            List<Object> list= (List<Object>)object;
+            writer.append("[");
+
+            int i = 0;
+            for (Object item : list) {
+                if (i > 0) {
+                    writer.append(", ");
+                }
+
+                writeObject(item, writer);
+                i++;
+            }
+
+            writer.append("]");
+        } else {
+            Map<String, Object> map;
+            if (object instanceof Map<?, ?>) {
+                map = (Map<String, Object>)object;
+            } else {
+                map = new BeanAdapter(object);
+            }
+
+            writer.append("{");
+
+            int i = 0;
+            for (Map.Entry<String, Object> entry : map.entrySet()) {
+                String key = entry.getKey();
+                Object value = entry.getValue();
+
+                boolean identifier = true;
+                StringBuilder keyStringBuilder = new StringBuilder();
+
+                for (int j = 0, n = key.length(); j < n; j++) {
+                    char c = key.charAt(j);
+                    identifier &= Character.isJavaIdentifierPart(c);
+
+                    if (c == '"') {
+                        keyStringBuilder.append('\\');
+                    }
+
+                    keyStringBuilder.append(c);
+                }
+
+                key = keyStringBuilder.toString();
+
+                if (i > 0) {
+                    writer.append(", ");
+                }
+
+                // Write the key
+                if (!identifier || alwaysDelimitMapKeys) {
+                    writer.append('"');
+                }
+
+                writer.append(key);
+
+                if (!identifier || alwaysDelimitMapKeys) {
+                    writer.append('"');
+                }
+
+                writer.append(": ");
+
+                // Write the value
+                writeObject(value, writer);
+
+                i++;
+            }
+
+            writer.append("}");
+        }
+
+        writer.flush();
+    }
+
+    @Override
+    public String getMIMEType(Object object) {
+        return MIME_TYPE + "; charset=" + charset.name();
+    }
+
+    /**
+     * Converts a JSON value to a Java object.
+     *
+     * @param json
+     * The JSON value.
+     *
+     * @return
+     * The parsed object.
+     */
+    public static Object parse(String json) throws SerializationException {
+        JSONSerializer jsonSerializer = new JSONSerializer();
+
+        Object object;
+        try {
+            object = jsonSerializer.readObject(new StringReader(json));
+        } catch(IOException exception) {
+            throw new RuntimeException(exception);
+        }
+
+        return object;
+    }
+
+    /**
+     * Converts a JSON value to a string.
+     *
+     * @param json
+     * The JSON value.
+     *
+     * @return
+     * The parsed string.
+     */
+    public static String parseString(String json) throws SerializationException {
+        return (String)parse(json);
+    }
+
+    /**
+     * Converts a JSON value to a number.
+     *
+     * @param json
+     * The JSON value.
+     *
+     * @return
+     * The parsed number.
+     */
+    public static Number parseNumber(String json) throws SerializationException {
+        return (Number)parse(json);
+    }
+
+    /**
+     * Converts a JSON value to a short.
+     *
+     * @param json
+     * The JSON value.
+     *
+     * @return
+     * The parsed short.
+     */
+    public static Short parseShort(String json) throws SerializationException {
+        return (Short)parse(json);
+    }
+
+    /**
+     * Converts a JSON value to a integer.
+     *
+     * @param json
+     * The JSON value.
+     *
+     * @return
+     * The parsed integer.
+     */
+    public static Integer parseInteger(String json) throws SerializationException {
+        return (Integer)parse(json);
+    }
+
+    /**
+     * Converts a JSON value to a long.
+     *
+     * @param json
+     * The JSON value.
+     *
+     * @return
+     * The parsed number.
+     */
+    public static Long parseLong(String json) throws SerializationException {
+        return (Long)parse(json);
+    }
+
+    /**
+     * Converts a JSON value to a float.
+     *
+     * @param json
+     * The JSON value.
+     *
+     * @return
+     * The parsed float.
+     */
+    public static Float parseFloat(String json) throws SerializationException {
+        return (Float)parse(json);
+    }
+
+    /**
+     * Converts a JSON value to a double.
+     *
+     * @param json
+     * The JSON value.
+     *
+     * @return
+     * The parsed double.
+     */
+    public static Double parseDouble(String json) throws SerializationException {
+        return (Double)parse(json);
+    }
+
+    /**
+     * Converts a JSON value to a boolean.
+     *
+     * @param json
+     * The JSON value.
+     *
+     * @return
+     * The parsed boolean.
+     */
+    public static Boolean parseBoolean(String json) throws SerializationException {
+        return (Boolean)parse(json);
+    }
+
+    /**
+     * Converts a JSON value to a list.
+     *
+     * @param json
+     * The JSON value.
+     *
+     * @return
+     * The parsed list.
+     */
+    public static List<?> parseList(String json) throws SerializationException {
+        return (List<?>)parse(json);
+    }
+
+    /**
+     * Converts a JSON value to a map.
+     *
+     * @param json
+     * The JSON value.
+     *
+     * @return
+     * The parsed map.
+     */
+    @SuppressWarnings("unchecked")
+    public static Map<String, ?> parseMap(String json) throws SerializationException {
+        return (Map<String, ?>)parse(json);
+    }
+
+    /**
+     * Converts a object to a JSON string representation. The map keys will always
+     * be quote-delimited.
+     *
+     * @param value
+     * The object to convert.
+     *
+     * @return
+     * The resulting JSON string.
+     *
+     * @see #toString(Object, boolean)
+     */
+    public static String toString(Object value) throws SerializationException {
+        return toString(value, false);
+    }
+
+    /**
+     * Converts a object to a JSON string representation.
+     *
+     * @param value
+     * The object to convert.
+     *
+     * @param alwaysDelimitMapKeys
+     * A flag indicating whether or not map keys will always be quote-delimited.
+     *
+     * @return
+     * The resulting JSON string.
+     */
+    public static String toString(Object value, boolean alwaysDelimitMapKeys) throws SerializationException {
+        JSONSerializer jsonSerializer = new JSONSerializer();
+        jsonSerializer.setAlwaysDelimitMapKeys(alwaysDelimitMapKeys);
+
+        StringWriter writer = new StringWriter();
+
+        try {
+            jsonSerializer.writeObject(value, writer);
+        } catch(IOException exception) {
+            throw new RuntimeException(exception);
+        }
+
+        return writer.toString();
+    }
+
+    public ListenerList<JSONSerializerListener> getJSONSerializerListeners() {
+        if (jsonSerializerListeners == null) {
+            jsonSerializerListeners = new JSONSerializerListenerList();
+        }
+
+        return jsonSerializerListeners;
+    }
+}

Added: pivot/branches/3.x/core/src/org/apache/pivot/json/JSONSerializerListener.java
URL: http://svn.apache.org/viewvc/pivot/branches/3.x/core/src/org/apache/pivot/json/JSONSerializerListener.java?rev=1057053&view=auto
==============================================================================
--- pivot/branches/3.x/core/src/org/apache/pivot/json/JSONSerializerListener.java (added)
+++ pivot/branches/3.x/core/src/org/apache/pivot/json/JSONSerializerListener.java Sun Jan  9 23:19:19 2011
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pivot.json;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * JSON serializer listener interface.
+ */
+public interface JSONSerializerListener {
+    /**
+     * JSON serializer listener adapter.
+     */
+    public static class Adapter implements JSONSerializerListener {
+        @Override
+        public void beginMap(JSONSerializer jsonSerializer, Map<String, ?> value) {
+        }
+
+        @Override
+        public void endMap(JSONSerializer jsonSerializer) {
+        }
+
+        @Override
+        public void readKey(JSONSerializer jsonSerializer, String key) {
+        }
+
+        @Override
+        public void beginList(JSONSerializer jsonSerializer, List<?> value) {
+        }
+
+        @Override
+        public void endList(JSONSerializer jsonSerializer) {
+        }
+
+        @Override
+        public void readString(JSONSerializer jsonSerializer, String value) {
+        }
+
+        @Override
+        public void readNumber(JSONSerializer jsonSerializer, Number value) {
+        }
+
+        @Override
+        public void readBoolean(JSONSerializer jsonSerializer, Boolean value) {
+        }
+
+        @Override
+        public void readNull(JSONSerializer jsonSerializer) {
+        }
+    }
+
+    /**
+     * Called when the serializer has begun reading a map value.
+     *
+     * @param jsonSerializer
+     * @param value
+     */
+    public void beginMap(JSONSerializer jsonSerializer, Map<String, ?> value);
+
+    /**
+     * Called when the serializer has finished reading a map value.
+     *
+     * @param jsonSerializer
+     */
+    public void endMap(JSONSerializer jsonSerializer);
+
+    /**
+     * Called when the serializer has read a map key.
+     *
+     * @param jsonSerializer
+     * @param key
+     */
+    public void readKey(JSONSerializer jsonSerializer, String key);
+
+    /**
+     * Called when the serializer has begun reading a list value.
+     *
+     * @param jsonSerializer
+     * @param value
+     */
+    public void beginList(JSONSerializer jsonSerializer, List<?> value);
+
+    /**
+     * Called when the serializer has finished reading a list value.
+     *
+     * @param jsonSerializer
+     */
+    public void endList(JSONSerializer jsonSerializer);
+
+    /**
+     * Called when the serializer has read a string value.
+     *
+     * @param jsonSerializer
+     * @param value
+     */
+    public void readString(JSONSerializer jsonSerializer, String value);
+
+    /**
+     * Called when the serializer has read a numeric value.
+     *
+     * @param jsonSerializer
+     * @param value
+     */
+    public void readNumber(JSONSerializer jsonSerializer, Number value);
+
+    /**
+     * Called when the serializer has read a boolean value.
+     *
+     * @param jsonSerializer
+     * @param value
+     */
+    public void readBoolean(JSONSerializer jsonSerializer, Boolean value);
+
+    /**
+     * Called when the serializer has read a null value.
+     *
+     * @param jsonSerializer
+     */
+    public void readNull(JSONSerializer jsonSerializer);
+}

Added: pivot/branches/3.x/core/src/org/apache/pivot/text/FileSizeFormat.java
URL: http://svn.apache.org/viewvc/pivot/branches/3.x/core/src/org/apache/pivot/text/FileSizeFormat.java?rev=1057053&view=auto
==============================================================================
--- pivot/branches/3.x/core/src/org/apache/pivot/text/FileSizeFormat.java (added)
+++ pivot/branches/3.x/core/src/org/apache/pivot/text/FileSizeFormat.java Sun Jan  9 23:19:19 2011
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pivot.text;
+
+import java.text.FieldPosition;
+import java.text.Format;
+import java.text.NumberFormat;
+import java.text.ParsePosition;
+
+/**
+ * Converts a file size into a human-readable representation using binary
+ * prefixes (1KB = 1024 bytes).
+ */
+public class FileSizeFormat extends Format {
+    private static final long serialVersionUID = 9126510513247641698L;
+
+    public static final int KILOBYTE = 1024;
+    public static final String[] ABBREVIATIONS = {"K", "M", "G", "T", "P", "E", "Z", "Y"};
+
+    private static final FileSizeFormat FILE_SIZE_FORMAT = new FileSizeFormat();
+
+    private FileSizeFormat() {
+    }
+
+    /**
+     * Formats a file size.
+     *
+     * @param object
+     * A <tt>Number</tt> containing the length of the file, in bytes. May be
+     * negative to indicate an unknown file size.
+     *
+     * @param stringBuffer
+     * The string buffer to which the formatted output will be appended.
+     *
+     * @param fieldPosition
+     * Not used.
+     *
+     * @return
+     * The original string buffer, with the formatted value appended.
+     */
+    @Override
+    public StringBuffer format(Object object, StringBuffer stringBuffer,
+        FieldPosition fieldPosition) {
+        Number number = (Number)object;
+
+        long length = number.longValue();
+
+        if (length >= 0) {
+            double size = length;
+
+            int i = -1;
+            do {
+                size /= KILOBYTE;
+                i++;
+            } while (size > KILOBYTE);
+
+            NumberFormat numberFormat = NumberFormat.getNumberInstance();
+            if (i == 0
+                && size > 1) {
+                numberFormat.setMaximumFractionDigits(0);
+            } else {
+                numberFormat.setMaximumFractionDigits(1);
+            }
+
+            stringBuffer.append(numberFormat.format(size) + " " + ABBREVIATIONS[i] + "B");
+        }
+
+        return stringBuffer;
+    }
+
+    /**
+     * This method is not supported.
+     *
+     * @throws UnsupportedOperationException
+     */
+    @Override
+    public Object parseObject(String arg0, ParsePosition arg1) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Returns a shared file size format instance.
+     *
+     * @return
+     * A shared file format size instance.
+     */
+    public static FileSizeFormat getInstance() {
+        return FILE_SIZE_FORMAT;
+    }
+}

Added: pivot/branches/3.x/core/src/org/apache/pivot/util/Base64.java
URL: http://svn.apache.org/viewvc/pivot/branches/3.x/core/src/org/apache/pivot/util/Base64.java?rev=1057053&view=auto
==============================================================================
--- pivot/branches/3.x/core/src/org/apache/pivot/util/Base64.java (added)
+++ pivot/branches/3.x/core/src/org/apache/pivot/util/Base64.java Sun Jan  9 23:19:19 2011
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pivot.util;
+
+/**
+ * Implements the "base64" binary encoding scheme as defined by
+ * <a href="http://tools.ietf.org/html/rfc2045">RFC 2045</a>.
+ */
+public final class Base64 {
+    private static final char[] lookup = new char[64];
+    private static final byte[] reverseLookup = new byte[256];
+
+    static {
+        // Populate the lookup array
+
+        for (int i = 0; i < 26; i++) {
+            lookup[i] = (char)('A' + i);
+        }
+
+        for (int i = 26, j = 0; i < 52; i++, j++) {
+            lookup[i] = (char)('a' + j);
+        }
+
+        for (int i = 52, j = 0; i < 62; i++, j++) {
+            lookup[i] = (char)('0' + j);
+        }
+
+        lookup[62] = '+';
+        lookup[63] = '/';
+
+        // Populate the reverse lookup array
+
+        for (int i = 0; i < 256; i++) {
+            reverseLookup[i] = -1;
+        }
+
+        for (int i = 'Z'; i >= 'A'; i--) {
+            reverseLookup[i] = (byte)(i - 'A');
+        }
+
+        for (int i = 'z'; i >= 'a'; i--) {
+            reverseLookup[i] = (byte)(i - 'a' + 26);
+        }
+
+        for (int i = '9'; i >= '0'; i--) {
+            reverseLookup[i] = (byte)(i - '0' + 52);
+        }
+
+        reverseLookup['+'] = 62;
+        reverseLookup['/'] = 63;
+        reverseLookup['='] = 0;
+    }
+
+    /**
+     * This class is not instantiable.
+     */
+    private Base64() {
+    }
+
+    /**
+     * Encodes the specified data into a base64 string.
+     *
+     * @param bytes
+     * The unencoded raw data.
+     */
+    public static String encode(byte[] bytes) {
+        StringBuilder buf = new StringBuilder(4 * (bytes.length / 3 + 1));
+
+        for (int i = 0, n = bytes.length; i < n; ) {
+            byte byte0 = bytes[i++];
+            byte byte1 = (i++ < n) ? bytes[i - 1] : 0;
+            byte byte2 = (i++ < n) ? bytes[i - 1] : 0;
+
+            buf.append(lookup[byte0 >> 2]);
+            buf.append(lookup[((byte0 << 4) | byte1 >> 4) & 63]);
+            buf.append(lookup[((byte1 << 2) | byte2 >> 6) & 63]);
+            buf.append(lookup[byte2 & 63]);
+
+            if (i > n) {
+                for (int m = buf.length(), j = m - (i - n); j < m; j++) {
+                    buf.setCharAt(j, '=');
+                }
+            }
+        }
+
+        return buf.toString();
+    }
+
+    /**
+     * Decodes the specified base64 string back into its raw data.
+     *
+     * @param encoded
+     * The base64 encoded string.
+     */
+    public static byte[] decode(String encoded) {
+        int padding = 0;
+
+        for (int i = encoded.length() - 1; encoded.charAt(i) == '='; i--) {
+            padding++;
+        }
+
+        int length = encoded.length() * 6 / 8 - padding;
+        byte[] bytes = new byte[length];
+
+        for (int i = 0, index = 0, n = encoded.length(); i < n; i += 4) {
+            int word = reverseLookup[encoded.charAt(i)] << 18;
+            word += reverseLookup[encoded.charAt(i + 1)] << 12;
+            word += reverseLookup[encoded.charAt(i + 2)] << 6;
+            word += reverseLookup[encoded.charAt(i + 3)];
+
+            for (int j = 0; j < 3 && index + j < length; j++) {
+                bytes[index + j] = (byte)(word >> (8 * (2 - j)));
+            }
+
+            index += 3;
+        }
+
+        return bytes;
+    }
+}

Added: pivot/branches/3.x/core/src/org/apache/pivot/util/CalendarDate.java
URL: http://svn.apache.org/viewvc/pivot/branches/3.x/core/src/org/apache/pivot/util/CalendarDate.java?rev=1057053&view=auto
==============================================================================
--- pivot/branches/3.x/core/src/org/apache/pivot/util/CalendarDate.java (added)
+++ pivot/branches/3.x/core/src/org/apache/pivot/util/CalendarDate.java Sun Jan  9 23:19:19 2011
@@ -0,0 +1,399 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pivot.util;
+
+import java.io.Serializable;
+import java.text.NumberFormat;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * <tt>CalendarDate</tt> allows a specific day to be identified within the
+ * Gregorian calendar system. This identification has no association with any
+ * particular time zone and no notion of the time of day.
+ */
+public final class CalendarDate implements Comparable<CalendarDate>, Serializable {
+    private static final long serialVersionUID = 0;
+
+    /**
+     * Represents a range of calendar dates.
+     */
+    public static final class Range {
+        public final CalendarDate start;
+        public final CalendarDate end;
+
+        public Range(CalendarDate calendarDate) {
+            this(calendarDate, calendarDate);
+        }
+
+        public Range(CalendarDate start, CalendarDate end) {
+            this.start = start;
+            this.end = end;
+        }
+
+        public Range(String start, String end) {
+            this.start = CalendarDate.decode(start);
+            this.end = CalendarDate.decode(end);
+        }
+
+        public Range(Range range) {
+            if (range == null) {
+                throw new IllegalArgumentException("range is null.");
+            }
+
+            start = range.start;
+            end = range.end;
+        }
+
+        public int getLength() {
+            return Math.abs(start.subtract(end)) + 1;
+        }
+
+        public boolean contains(Range range) {
+            if (range == null) {
+                throw new IllegalArgumentException("range is null.");
+            }
+
+            Range normalizedRange = range.normalize();
+
+            boolean contains;
+            if (start.compareTo(end) < 0) {
+                contains = (start.compareTo(normalizedRange.start) <= 0
+                    && end.compareTo(normalizedRange.end) >= 0);
+            } else {
+                contains = (end.compareTo(normalizedRange.start) <= 0
+                    && start.compareTo(normalizedRange.end) >= 0);
+            }
+
+            return contains;
+        }
+
+        public boolean contains(CalendarDate calendarDate) {
+            if (calendarDate == null) {
+                throw new IllegalArgumentException("calendarDate is null.");
+            }
+
+            boolean contains;
+            if (start.compareTo(end) < 0) {
+                contains = (start.compareTo(calendarDate) <= 0
+                    && end.compareTo(calendarDate) >= 0);
+            } else {
+                contains = (end.compareTo(calendarDate) <= 0
+                    && start.compareTo(calendarDate) >= 0);
+            }
+
+            return contains;
+        }
+
+        public boolean intersects(Range range) {
+            if (range == null) {
+                throw new IllegalArgumentException("range is null.");
+            }
+
+            Range normalizedRange = range.normalize();
+
+            boolean intersects;
+            if (start.compareTo(end) < 0) {
+                intersects = (start.compareTo(normalizedRange.end) <= 0
+                    && end.compareTo(normalizedRange.start) >= 0);
+            } else {
+                intersects = (end.compareTo(normalizedRange.end) <= 0
+                    && start.compareTo(normalizedRange.start) >= 0);
+            }
+
+            return intersects;
+        }
+
+        public Range normalize() {
+            CalendarDate earlier = (start.compareTo(end) < 0 ? start : end);
+            CalendarDate later = (earlier == start ? end : start);
+            return new Range(earlier, later);
+        }
+    }
+
+    /**
+     * The year field. (e.g. <tt>2008</tt>).
+     */
+    public final int year;
+
+    /**
+     * The month field, 0-based. (e.g. <tt>2</tt> for March).
+     */
+    public final int month;
+
+    /**
+     * The day of the month, 0-based. (e.g. <tt>14</tt> for the 15th).
+     */
+    public final int day;
+
+    private static final int[] MONTH_LENGTHS = {
+        31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
+    };
+
+    private static final int GREGORIAN_CUTOVER_YEAR = 1582;
+    private static final Pattern PATTERN = Pattern.compile("^(\\d{4})-(\\d{2})-(\\d{2})$");
+
+    /**
+     * Creates a new <tt>CalendarDate</tt> representing the current day in the
+     * default timezone and the default locale.
+     */
+    public CalendarDate() {
+        this(new GregorianCalendar());
+    }
+
+    /**
+     * Creates a new <tt>CalendarDate</tt> representing the day contained in
+     * the specified Gregorian calendar (assuming the default locale and the
+     * default timezone).
+     *
+     * @param calendar
+     * The calendar containing the year, month, and day fields.
+     */
+    public CalendarDate(GregorianCalendar calendar) {
+        this(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH),
+            calendar.get(Calendar.DAY_OF_MONTH) - 1);
+    }
+
+    /**
+     * Creates a new <tt>CalendarDate</tt> representing the specified year,
+     * month, and day of month.
+     *
+     * @param year
+     * The year field. (e.g. <tt>2008</tt>)
+     *
+     * @param month
+     * The month field, 0-based. (e.g. <tt>2</tt> for March)
+     *
+     * @param day
+     * The day of the month, 0-based. (e.g. <tt>14</tt> for the 15th)
+     */
+    public CalendarDate(int year, int month, int day) {
+        if (year <= GREGORIAN_CUTOVER_YEAR || year > 9999) {
+            throw new IllegalArgumentException("Invalid year: " + year);
+        }
+
+        if (month < 0 || month > 11) {
+            throw new IllegalArgumentException("Invalid month: " + month);
+        }
+
+        int daysInMonth = MONTH_LENGTHS[month];
+
+        boolean isLeapYear = ((year & 3) == 0 && (year % 100 != 0 || year % 400 == 0));
+        if (isLeapYear && month == 1) {
+            daysInMonth++;
+        }
+
+        if (day < 0 || day >= daysInMonth) {
+            throw new IllegalArgumentException("Invalid day: " + day);
+        }
+
+        this.year = year;
+        this.month = month;
+        this.day = day;
+    }
+
+    /**
+     * Adds the specified number of days to this calendar date and returns the
+     * resulting calendar date. The number of days may be negative, in which
+     * case the result will be a date before this calendar date.
+     * <p>
+     * More formally, it is defined that given calendar dates <tt>c1</tt> and
+     * <tt>c2</tt>, the following will return <tt>true</tt>:
+     * <pre>
+     *    c1.add(c2.subtract(c1)).equals(c2);
+     * </pre>
+     *
+     * @param days
+     * The number of days to add to (or subtract from if negative) this
+     * calendar date.
+     *
+     * @return
+     * The resulting calendar date.
+     */
+    public CalendarDate add(int days) {
+        GregorianCalendar calendar = toCalendar();
+        calendar.add(Calendar.DAY_OF_YEAR, days);
+        return new CalendarDate(calendar);
+    }
+
+    /**
+     * Gets the number of days in between this calendar date and the specified
+     * calendar date. If this calendar date represents a day after the
+     * specified calendar date, the difference will be positive. If this
+     * calendar date represents a day before the specified calendar date, the
+     * difference will be negative. If the two calendar dates represent the
+     * same day, the difference will be zero.
+     * <p>
+     * More formally, it is defined that given calendar dates <tt>c1</tt> and
+     * <tt>c2</tt>, the following will return <tt>true</tt>:
+     * <pre>
+     *    c1.add(c2.subtract(c1)).equals(c2);
+     * </pre>
+     *
+     * @param calendarDate
+     * The calendar date to subtract from this calendar date.
+     *
+     * @return
+     * The number of days in between this calendar date and
+     * <tt>calendarDate</tt>.
+     */
+    public int subtract(CalendarDate calendarDate) {
+        GregorianCalendar c1 = toCalendar();
+        GregorianCalendar c2 = calendarDate.toCalendar();
+
+        long t1 = c1.getTimeInMillis();
+        long t2 = c2.getTimeInMillis();
+
+        return (int)((t1 - t2) / (1000 * 60 * 60 * 24));
+    }
+
+    /**
+     * Translates this calendar date to an instance of
+     * <tt>GregorianCalendar</tt>, with the <tt>year</tt>, <tt>month</tt>, and
+     * <tt>dayOfMonth</tt> fields set in the default time zone with the default
+     * locale.
+     *
+     * @return
+     * This calendar date as a <tt>GregorianCalendar</tt>.
+     */
+    public GregorianCalendar toCalendar() {
+        return toCalendar(new Time(0, 0, 0));
+    }
+
+    /**
+     * Translates this calendar date to an instance of
+     * <tt>GregorianCalendar</tt>, with the <tt>year</tt>, <tt>month</tt>, and
+     * <tt>dayOfMonth</tt> fields set in the default time zone with the default
+     * locale.
+     *
+     * @param time
+     * The time of day.
+     *
+     * @return
+     * This calendar date as a <tt>GregorianCalendar</tt>.
+     */
+    public GregorianCalendar toCalendar(Time time) {
+        GregorianCalendar calendar = new GregorianCalendar(year, month, day + 1,
+            time.hour, time.minute, time.second);
+        calendar.set(Calendar.MILLISECOND, time.millisecond);
+
+        return calendar;
+    }
+
+    /**
+     * Compares this calendar date with another calendar date.
+     *
+     * @param calendarDate
+     * The calendar date against which to compare.
+     *
+     * @return
+     * A negative number, zero, or a positive number if the specified calendar
+     * date is less than, equal to, or greater than this calendar date,
+     * respectively.
+     */
+    @Override
+    public int compareTo(CalendarDate calendarDate) {
+        int result = year - calendarDate.year;
+
+        if (result == 0) {
+            result = month - calendarDate.month;
+
+            if (result == 0) {
+                result = day - calendarDate.day;
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Indicates whether some other object is "equal to" this one.
+     * This is the case if the object is a calendar date that represents the
+     * same day as this one.
+     *
+     * @param o
+     * Reference to the object against which to compare.
+     */
+    @Override
+    public boolean equals(Object o) {
+        return (o instanceof CalendarDate
+            && ((CalendarDate)o).year == year
+            && ((CalendarDate)o).month == month
+            && ((CalendarDate)o).day == day);
+    }
+
+    /**
+     * Returns a hash code value for the object.
+     */
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + year;
+        result = prime * result + month;
+        result = prime * result + day;
+        return result;
+    }
+
+    /**
+     * Returns a string representation of this calendar date in the <tt>ISO
+     * 8601</tt> "calendar date" format, which is <tt>[YYYY]-[MM]-[DD]</tt>.
+     */
+    @Override
+    public String toString() {
+        StringBuilder buf = new StringBuilder();
+
+        NumberFormat format = NumberFormat.getIntegerInstance();
+        format.setGroupingUsed(false);
+        format.setMinimumIntegerDigits(4);
+
+        buf.append(format.format(year));
+        buf.append("-");
+
+        format.setMinimumIntegerDigits(2);
+
+        buf.append(format.format(month + 1));
+        buf.append("-");
+        buf.append(format.format(day + 1));
+
+        return buf.toString();
+    }
+
+    /**
+     * Creates a new date representing the specified date string. The date
+     * string must be in the <tt>ISO 8601</tt> "calendar date" format,
+     * which is <tt>[YYYY]-[MM]-[DD]</tt>.
+     *
+     * @param value
+     * A string in the form of <tt>[YYYY]-[MM]-[DD]</tt> (e.g. 2008-07-23).
+     */
+    public static CalendarDate decode(String value) {
+        Matcher matcher = PATTERN.matcher(value);
+
+        if (!matcher.matches()) {
+            throw new IllegalArgumentException("Invalid date format: " + value);
+        }
+
+        int year = Integer.parseInt(matcher.group(1));
+        int month = Integer.parseInt(matcher.group(2)) - 1;
+        int day = Integer.parseInt(matcher.group(3)) - 1;
+
+        return new CalendarDate(year, month, day);
+    }
+}