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);
+ }
+}