From 4edc7738ec08da5fc3f8930fa5f496d776191f91 Mon Sep 17 00:00:00 2001 From: Edward Capriolo Date: Fri, 9 Jan 2015 11:33:54 -0500 Subject: [PATCH] Initial code merge --- pom.xml | 183 ++ .../com/google/code/gossip/GossipMember.java | 138 ++ .../com/google/code/gossip/GossipRunner.java | 46 + .../com/google/code/gossip/GossipService.java | 77 + .../google/code/gossip/GossipSettings.java | 64 + .../code/gossip/GossipTimeoutTimer.java | 61 + .../google/code/gossip/LocalGossipMember.java | 42 + .../java/com/google/code/gossip/LogLevel.java | 25 + .../code/gossip/RemoteGossipMember.java | 28 + .../google/code/gossip/StartupSettings.java | 161 ++ .../code/gossip/examples/GossipExample.java | 78 + .../gossip/manager/ActiveGossipThread.java | 59 + .../code/gossip/manager/GossipManager.java | 189 ++ .../gossip/manager/PassiveGossipThread.java | 145 ++ ...nlyProcessReceivedPassiveGossipThread.java | 93 + .../impl/SendMembersActiveGossipThread.java | 106 ++ .../random/RandomActiveGossipThread.java | 44 + .../manager/random/RandomGossipManager.java | 14 + src/main/java/org/json/CDL.java | 279 +++ src/main/java/org/json/JSONArray.java | 918 ++++++++++ src/main/java/org/json/JSONException.java | 31 + src/main/java/org/json/JSONML.java | 455 +++++ src/main/java/org/json/JSONObject.java | 1583 +++++++++++++++++ src/main/java/org/json/JSONString.java | 18 + src/main/java/org/json/JSONStringer.java | 78 + src/main/java/org/json/JSONTokener.java | 435 +++++ src/main/java/org/json/JSONWriter.java | 323 ++++ src/main/java/org/json/XML.java | 441 +++++ src/main/java/org/json/XMLTokener.java | 365 ++++ 29 files changed, 6479 insertions(+) create mode 100644 pom.xml create mode 100644 src/main/java/com/google/code/gossip/GossipMember.java create mode 100644 src/main/java/com/google/code/gossip/GossipRunner.java create mode 100644 src/main/java/com/google/code/gossip/GossipService.java create mode 100644 src/main/java/com/google/code/gossip/GossipSettings.java create mode 100644 src/main/java/com/google/code/gossip/GossipTimeoutTimer.java create mode 100644 src/main/java/com/google/code/gossip/LocalGossipMember.java create mode 100644 src/main/java/com/google/code/gossip/LogLevel.java create mode 100644 src/main/java/com/google/code/gossip/RemoteGossipMember.java create mode 100644 src/main/java/com/google/code/gossip/StartupSettings.java create mode 100644 src/main/java/com/google/code/gossip/examples/GossipExample.java create mode 100644 src/main/java/com/google/code/gossip/manager/ActiveGossipThread.java create mode 100644 src/main/java/com/google/code/gossip/manager/GossipManager.java create mode 100644 src/main/java/com/google/code/gossip/manager/PassiveGossipThread.java create mode 100644 src/main/java/com/google/code/gossip/manager/impl/OnlyProcessReceivedPassiveGossipThread.java create mode 100644 src/main/java/com/google/code/gossip/manager/impl/SendMembersActiveGossipThread.java create mode 100644 src/main/java/com/google/code/gossip/manager/random/RandomActiveGossipThread.java create mode 100644 src/main/java/com/google/code/gossip/manager/random/RandomGossipManager.java create mode 100644 src/main/java/org/json/CDL.java create mode 100644 src/main/java/org/json/JSONArray.java create mode 100644 src/main/java/org/json/JSONException.java create mode 100644 src/main/java/org/json/JSONML.java create mode 100644 src/main/java/org/json/JSONObject.java create mode 100644 src/main/java/org/json/JSONString.java create mode 100644 src/main/java/org/json/JSONStringer.java create mode 100644 src/main/java/org/json/JSONTokener.java create mode 100644 src/main/java/org/json/JSONWriter.java create mode 100644 src/main/java/org/json/XML.java create mode 100644 src/main/java/org/json/XMLTokener.java diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..e3accfb --- /dev/null +++ b/pom.xml @@ -0,0 +1,183 @@ + + + 4.0.0 + io.teknek + gossip + gossip + 0.0.0-SNAPSHOT + A peer to peer cluster process + jar + + + + + + org.sonatype.oss + oss-parent + 7 + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + https://github.com/edwardcapriolo/teknek-core + https://github.com/edwardcapriolo/teknek-core.git + + + scm:git:git@github.com:edwardcapriolo/teknek-core.git + + + + + junit + junit + 4.8.2 + test + + + + log4j + log4j + 1.2.15 + jar + compile + + + javax.jms + jms + + + com.sun.jdmk + jmxtools + + + com.sun.jmx + jmxri + + + + + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + + + sign-artifacts + verify + + sign + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.2 + + + + + + + test-jar + + + + + + org.apache.maven.plugins + maven-eclipse-plugin + 2.5.1 + + [artifactId] + true + true + 1.5 + + org.eclipse.jdt.core.javabuilder + org.maven.ide.eclipse.maven2Builder + + + org.eclipse.jdt.core.javanature + org.maven.ide.eclipse.maven2Nature + + + + + maven-compiler-plugin + + 1.7 + 1.7 + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.4 + + + + + + + + + + ecapriolo + Edward Capriolo + edlinuxguru@gmail.com + + + + + developer + + -6 + + + + + release-sign-artifacts + + + performRelease + true + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.1 + + + sign-artifacts + verify + + sign + + + + + + + + + + diff --git a/src/main/java/com/google/code/gossip/GossipMember.java b/src/main/java/com/google/code/gossip/GossipMember.java new file mode 100644 index 0000000..7e3835a --- /dev/null +++ b/src/main/java/com/google/code/gossip/GossipMember.java @@ -0,0 +1,138 @@ +package com.google.code.gossip; + +import java.net.InetSocketAddress; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * A abstract class representing a gossip member. + * + * @author joshclemm, harmenw + */ +public abstract class GossipMember { + /** The JSON key for the host property. */ + public static final String JSON_HOST = "host"; + /** The JSON key for the port property. */ + public static final String JSON_PORT = "port"; + /** The JSON key for the heartbeat property. */ + public static final String JSON_HEARTBEAT = "heartbeat"; + + /** The hostname or IP address of this gossip member. */ + protected String _host; + + /** The port number of this gossip member. */ + protected int _port; + + /** The current heartbeat of this gossip member. */ + protected int _heartbeat; + + /** + * Constructor. + * @param host The hostname or IP address. + * @param port The port number. + * @param heartbeat The current heartbeat. + */ + public GossipMember(String host, int port, int heartbeat) { + _host = host; + _port = port; + _heartbeat = heartbeat; + } + + /** + * Get the hostname or IP address of the remote gossip member. + * @return The hostname or IP address. + */ + public String getHost() { + return _host; + } + + /** + * Get the port number of the remote gossip member. + * @return The port number. + */ + public int getPort() { + return _port; + } + + /** + * The member address in the form IP/host:port + * Similar to the toString in {@link InetSocketAddress} + */ + public String getAddress() { + return _host+":"+_port; + } + + /** + * Get the heartbeat of this gossip member. + * @return The current heartbeat. + */ + public int getHeartbeat() { + return _heartbeat; + } + + /** + * Set the heartbeat of this gossip member. + * @param heartbeat The new heartbeat. + */ + public void setHeartbeat(int heartbeat) { + this._heartbeat = heartbeat; + } + + /** + * @see java.lang.Object#toString() + */ + public String toString() { + return "Member [address=" + getAddress() + ", heartbeat=" + _heartbeat + "]"; + } + + /** + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + String address = getAddress(); + result = prime * result + + ((address == null) ? 0 : address.hashCode()); + return result; + } + + /** + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + System.err.println("equals(): obj is null."); + return false; + } + if (! (obj instanceof GossipMember) ) { + System.err.println("equals(): obj is not of type GossipMember."); + return false; + } + // The object is the same of they both have the same address (hostname and port). + return getAddress().equals(((LocalGossipMember) obj).getAddress()); + } + + /** + * Get the JSONObject which is the JSON representation of this GossipMember. + * @return The JSONObject of this GossipMember. + */ + public JSONObject toJSONObject() { + try { + JSONObject jsonObject = new JSONObject(); + jsonObject.put(JSON_HOST, _host); + jsonObject.put(JSON_PORT, _port); + jsonObject.put(JSON_HEARTBEAT, _heartbeat); + return jsonObject; + } catch (JSONException e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/src/main/java/com/google/code/gossip/GossipRunner.java b/src/main/java/com/google/code/gossip/GossipRunner.java new file mode 100644 index 0000000..95d2bb8 --- /dev/null +++ b/src/main/java/com/google/code/gossip/GossipRunner.java @@ -0,0 +1,46 @@ +package com.google.code.gossip; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +import org.json.JSONException; + +public class GossipRunner { + private StartupSettings _settings; + + public static void main(String[] args) { + File configFile; + + if (args.length == 1) { + configFile = new File("./"+args[0]); + } else { + configFile = new File("gossip.conf"); + } + + new GossipRunner(configFile); + } + + public GossipRunner(File configFile) { + + if (configFile != null && configFile.exists()) { + try { + System.out.println("Parsing the configuration file..."); + _settings = StartupSettings.fromJSONFile(configFile); + GossipService gossipService = new GossipService(_settings); + System.out.println("Gossip service successfully inialized, let's start it..."); + gossipService.start(); + } catch (FileNotFoundException e) { + System.err.println("The given file is not found!"); + } catch (JSONException e) { + System.err.println("The given file is not in the correct JSON format!"); + } catch (IOException e) { + System.err.println("Could not read the configuration file: " + e.getMessage()); + } catch (InterruptedException e) { + System.err.println("Error while starting the gossip service: " + e.getMessage()); + } + } else { + System.out.println("The gossip.conf file is not found.\n\nEither specify the path to the startup settings file or place the gossip.json file in the same folder as the JAR file."); + } + } +} diff --git a/src/main/java/com/google/code/gossip/GossipService.java b/src/main/java/com/google/code/gossip/GossipService.java new file mode 100644 index 0000000..0fea049 --- /dev/null +++ b/src/main/java/com/google/code/gossip/GossipService.java @@ -0,0 +1,77 @@ +package com.google.code.gossip; + +import java.io.PrintStream; +import java.net.InetAddress; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Date; + +import com.google.code.gossip.manager.GossipManager; +import com.google.code.gossip.manager.random.RandomGossipManager; + +/** + * This object represents the service which is responsible for gossiping with other gossip members. + * + * @author joshclemm, harmenw + */ +public class GossipService { + + /** A instance variable holding the log level. */ + private int _logLevel = LogLevel.INFO; + + private GossipManager _gossipManager; + + /** + * Constructor with the default settings. + * @throws InterruptedException + * @throws UnknownHostException + */ + public GossipService(StartupSettings startupSettings) throws InterruptedException, UnknownHostException { + this(InetAddress.getLocalHost().getHostAddress(), startupSettings.getPort(), startupSettings.getLogLevel(), startupSettings.getGossipMembers(), startupSettings.getGossipSettings()); + } + + /** + * Setup the client's lists, gossiping parameters, and parse the startup config file. + * @throws SocketException + * @throws InterruptedException + * @throws UnknownHostException + */ + public GossipService(String ipAddress, int port, int logLevel, ArrayList gossipMembers, GossipSettings settings) throws InterruptedException, UnknownHostException { + // Set the logging level. + _logLevel = logLevel; + + _gossipManager = new RandomGossipManager(ipAddress, port, settings, gossipMembers); + } + + public void start() { + _gossipManager.start(); + } + + public void shutdown() { + _gossipManager.shutdown(); + } + + public static void error(Object message) { + //if (_logLevel >= LogLevel.ERROR) printMessage(message, System.err); + printMessage(message, System.err); + } + + public static void info(Object message) { + //if (_logLevel >= LogLevel.INFO) printMessage(message, System.out); + printMessage(message, System.out); + } + + public static void debug(Object message) { + //if (_logLevel >= LogLevel.DEBUG) printMessage(message, System.out); + printMessage(message, System.out); + } + + private static void printMessage(Object message, PrintStream out) { + /**String addressString = "unknown"; + if (_me != null) + addressString = _me.getAddress(); + out.println("[" + addressString + "][" + new Date().toString() + "] " + message);*/ + out.println("[" + new Date().toString() + "] " + message); + } +} diff --git a/src/main/java/com/google/code/gossip/GossipSettings.java b/src/main/java/com/google/code/gossip/GossipSettings.java new file mode 100644 index 0000000..f81f458 --- /dev/null +++ b/src/main/java/com/google/code/gossip/GossipSettings.java @@ -0,0 +1,64 @@ +package com.google.code.gossip; + +/** + * In this object the settings used by the GossipService are held. + * + * @author harmenw + */ +public class GossipSettings { + + /** Time between gossip'ing in ms. Default is 1 second. */ + private int _gossipInterval = 1000; + + /** Time between cleanups in ms. Default is 10 seconds. */ + private int _cleanupInterval = 10000; + + /** + * Construct GossipSettings with default settings. + */ + public GossipSettings() {} + + /** + * Construct GossipSettings with given settings. + * @param gossipInterval The gossip interval in ms. + * @param cleanupInterval The cleanup interval in ms. + */ + public GossipSettings(int gossipInterval, int cleanupInterval) { + _gossipInterval = gossipInterval; + _cleanupInterval = cleanupInterval; + } + + /** + * Set the gossip interval. + * This is the time between a gossip message is send. + * @param gossipInterval The gossip interval in ms. + */ + public void setGossipTimeout(int gossipInterval) { + _gossipInterval = gossipInterval; + } + + /** + * Set the cleanup interval. + * This is the time between the last heartbeat received from a member and when it will be marked as dead. + * @param cleanupInterval The cleanup interval in ms. + */ + public void setCleanupInterval(int cleanupInterval) { + _cleanupInterval = cleanupInterval; + } + + /** + * Get the gossip interval. + * @return The gossip interval in ms. + */ + public int getGossipInterval() { + return _gossipInterval; + } + + /** + * Get the clean interval. + * @return The cleanup interval. + */ + public int getCleanupInterval() { + return _cleanupInterval; + } +} diff --git a/src/main/java/com/google/code/gossip/GossipTimeoutTimer.java b/src/main/java/com/google/code/gossip/GossipTimeoutTimer.java new file mode 100644 index 0000000..5f4eca0 --- /dev/null +++ b/src/main/java/com/google/code/gossip/GossipTimeoutTimer.java @@ -0,0 +1,61 @@ +package com.google.code.gossip; + +import java.util.Date; + +import javax.management.NotificationListener; +import javax.management.timer.Timer; + +/** + * This object represents a timer for a gossip member. + * When the timer has elapsed without being reset in the meantime, it will inform the GossipService about this + * who in turn will put the gossip member on the dead list, because it is apparantly not alive anymore. + * + * @author joshclemm, harmenw + */ +public class GossipTimeoutTimer extends Timer { + + /** The amount of time this timer waits before generating a wake-up event. */ + private long _sleepTime; + + /** The gossip member this timer is for. */ + private LocalGossipMember _source; + + /** + * Constructor. + * Creates a reset-able timer that wakes up after millisecondsSleepTime. + * @param millisecondsSleepTime The time for this timer to wait before an event. + * @param service + * @param member + */ + public GossipTimeoutTimer(long millisecondsSleepTime, NotificationListener notificationListener, LocalGossipMember member) { + super(); + _sleepTime = millisecondsSleepTime; + _source = member; + addNotificationListener(notificationListener, null, null); + } + + /** + * @see javax.management.timer.Timer#start() + */ + public void start() { + this.reset(); + super.start(); + } + + /** + * Resets timer to start counting down from original time. + */ + public void reset() { + removeAllNotifications(); + setWakeupTime(_sleepTime); + } + + /** + * Adds a new wake-up time for this timer. + * @param milliseconds + */ + private void setWakeupTime(long milliseconds) { + addNotification("type", "message", _source, new Date(System.currentTimeMillis()+milliseconds)); + } +} + diff --git a/src/main/java/com/google/code/gossip/LocalGossipMember.java b/src/main/java/com/google/code/gossip/LocalGossipMember.java new file mode 100644 index 0000000..429f39e --- /dev/null +++ b/src/main/java/com/google/code/gossip/LocalGossipMember.java @@ -0,0 +1,42 @@ +package com.google.code.gossip; + +import javax.management.NotificationListener; + +/** + * This object represent a gossip member with the properties known locally. + * These objects are stored in the local list of gossip member.s + * + * @author harmenw + */ +public class LocalGossipMember extends GossipMember { + /** The timeout timer for this gossip member. */ + private transient GossipTimeoutTimer timeoutTimer; + + /** + * Constructor. + * @param host The hostname or IP address. + * @param port The port number. + * @param heartbeat The current heartbeat. + * @param gossipService The GossipService object. + * @param cleanupTimeout The cleanup timeout for this gossip member. + */ + public LocalGossipMember(String hostname, int port, int heartbeat, NotificationListener notificationListener, int cleanupTimeout) { + super(hostname, port, heartbeat); + + this.timeoutTimer = new GossipTimeoutTimer(cleanupTimeout, notificationListener, this); + } + + /** + * Start the timeout timer. + */ + public void startTimeoutTimer() { + this.timeoutTimer.start(); + } + + /** + * Reset the timeout timer. + */ + public void resetTimeoutTimer() { + this.timeoutTimer.reset(); + } +} diff --git a/src/main/java/com/google/code/gossip/LogLevel.java b/src/main/java/com/google/code/gossip/LogLevel.java new file mode 100644 index 0000000..16189ea --- /dev/null +++ b/src/main/java/com/google/code/gossip/LogLevel.java @@ -0,0 +1,25 @@ +package com.google.code.gossip; + +public class LogLevel { + + public static final String CONFIG_ERROR = "ERROR"; + + public static final String CONFIG_INFO = "INFO"; + + public static final String CONFIG_DEBUG = "DEBUG"; + + public static final int ERROR = 1; + public static final int INFO = 2; + public static final int DEBUG = 3; + + public static int fromString(String logLevel) { + if (logLevel.equals(CONFIG_ERROR)) + return ERROR; + else if (logLevel.equals(CONFIG_INFO)) + return INFO; + else if (logLevel.equals(CONFIG_DEBUG)) + return DEBUG; + else + return INFO; + } +} diff --git a/src/main/java/com/google/code/gossip/RemoteGossipMember.java b/src/main/java/com/google/code/gossip/RemoteGossipMember.java new file mode 100644 index 0000000..a133adc --- /dev/null +++ b/src/main/java/com/google/code/gossip/RemoteGossipMember.java @@ -0,0 +1,28 @@ +package com.google.code.gossip; + +/** + * The object represents a gossip member with the properties as received from a remote gossip member. + * + * @author harmenw + */ +public class RemoteGossipMember extends GossipMember { + + /** + * Constructor. + * @param host The hostname or IP address. + * @param port The port number. + * @param heartbeat The current heartbeat. + */ + public RemoteGossipMember(String hostname, int port, int heartbeat) { + super(hostname, port, heartbeat); + } + + /** + * Construct a RemoteGossipMember with a heartbeat of 0. + * @param host The hostname or IP address. + * @param port The port number. + */ + public RemoteGossipMember(String hostname, int port) { + super(hostname, port, 0); + } +} diff --git a/src/main/java/com/google/code/gossip/StartupSettings.java b/src/main/java/com/google/code/gossip/StartupSettings.java new file mode 100644 index 0000000..442034c --- /dev/null +++ b/src/main/java/com/google/code/gossip/StartupSettings.java @@ -0,0 +1,161 @@ +package com.google.code.gossip; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * This object represents the settings used when starting the gossip service. + * + * @author harmenw + */ +public class StartupSettings { + + /** The port to start the gossip service on. */ + private int _port; + + /** The logging level of the gossip service. */ + private int _logLevel; + + /** The gossip settings used at startup. */ + private GossipSettings _gossipSettings; + + /** The list with gossip members to start with. */ + private ArrayList _gossipMembers; + + /** + * Constructor. + * @param port The port to start the service on. + */ + public StartupSettings(int port, int logLevel) { + this(port, logLevel, new GossipSettings()); + } + + /** + * Constructor. + * @param port The port to start the service on. + */ + public StartupSettings(int port, int logLevel, GossipSettings gossipSettings) { + _port = port; + _logLevel = logLevel; + _gossipSettings = gossipSettings; + _gossipMembers = new ArrayList(); + } + + /** + * Set the port of the gossip service. + * @param port The port for the gossip service. + */ + public void setPort(int port) { + _port = port; + } + + /** + * Get the port for the gossip service. + * @return The port of the gossip service. + */ + public int getPort() { + return _port; + } + + /** + * Set the log level of the gossip service. + * @param logLevel The log level({LogLevel}). + */ + public void setLogLevel(int logLevel) { + _logLevel = logLevel; + } + + /** + * Get the log level of the gossip service. + * @return The log level. + */ + public int getLogLevel() { + return _logLevel; + } + + /** + * Get the GossipSettings. + * @return The GossipSettings object. + */ + public GossipSettings getGossipSettings() { + return _gossipSettings; + } + + /** + * Add a gossip member to the list of members to start with. + * @param member The member to add. + */ + public void addGossipMember(GossipMember member) { + _gossipMembers.add(member); + } + + /** + * Get the list with gossip members. + * @return The gossip members. + */ + public ArrayList getGossipMembers() { + return _gossipMembers; + } + + /** + * Parse the settings for the gossip service from a JSON file. + * @param jsonFile The file object which refers to the JSON config file. + * @return The StartupSettings object with the settings from the config file. + * @throws JSONException Thrown when the file is not well-formed JSON. + * @throws FileNotFoundException Thrown when the file cannot be found. + * @throws IOException Thrown when reading the file gives problems. + */ + public static StartupSettings fromJSONFile(File jsonFile) throws JSONException, FileNotFoundException, IOException { + // Read the file to a String. + BufferedReader br = new BufferedReader(new FileReader(jsonFile)); + StringBuffer buffer = new StringBuffer(); + String line; + while((line = br.readLine()) != null) { + buffer.append(line.trim()); + } + + // Lets parse the String as JSON. + JSONObject jsonObject = new JSONArray(buffer.toString()).getJSONObject(0); + + // Now get the port number. + int port = jsonObject.getInt("port"); + + // Get the log level from the config file. + int logLevel = LogLevel.fromString(jsonObject.getString("log_level")); + + // Get the gossip_interval from the config file. + int gossipInterval = jsonObject.getInt("gossip_interval"); + + // Get the cleanup_interval from the config file. + int cleanupInterval = jsonObject.getInt("cleanup_interval"); + + System.out.println("Config [port: " + port + ", log_level: " + logLevel + ", gossip_interval: " + gossipInterval + ", cleanup_interval: " + cleanupInterval + "]"); + + // Initiate the settings with the port number. + StartupSettings settings = new StartupSettings(port, logLevel, new GossipSettings(gossipInterval, cleanupInterval)); + + // Now iterate over the members from the config file and add them to the settings. + System.out.print("Config-members ["); + JSONArray membersJSON = jsonObject.getJSONArray("members"); + for (int i=0; i clients = new ArrayList(); + + // Get my ip address. + String myIpAddress = InetAddress.getLocalHost().getHostAddress(); + + // Create the gossip members and put them in a list and give them a port number starting with 2000. + ArrayList startupMembers = new ArrayList(); + for (int i=0; i memberList); + + /** + * Abstract method which should be implemented by a subclass. + * This method should return a member of the list to gossip with. + * @param memberList The list of members which are stored in the local list of members. + * @return The chosen LocalGossipMember to gossip with. + */ + abstract protected LocalGossipMember selectPartner(ArrayList memberList); +} diff --git a/src/main/java/com/google/code/gossip/manager/GossipManager.java b/src/main/java/com/google/code/gossip/manager/GossipManager.java new file mode 100644 index 0000000..3324583 --- /dev/null +++ b/src/main/java/com/google/code/gossip/manager/GossipManager.java @@ -0,0 +1,189 @@ +package com.google.code.gossip.manager; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.management.Notification; +import javax.management.NotificationListener; + +import com.google.code.gossip.GossipMember; +import com.google.code.gossip.GossipService; +import com.google.code.gossip.GossipSettings; +import com.google.code.gossip.LocalGossipMember; + +public abstract class GossipManager extends Thread implements NotificationListener { + /** The maximal number of bytes the packet with the GOSSIP may be. (Default is 100 kb) */ + public static final int MAX_PACKET_SIZE = 102400; + + /** The list of members which are in the gossip group (not including myself). */ + private ArrayList _memberList; + + /** The list of members which are known to be dead. */ + private ArrayList _deadList; + + /** The member I am representing. */ + private LocalGossipMember _me; + + /** The settings for gossiping. */ + private GossipSettings _settings; + + /** A boolean whether the gossip service should keep running. */ + private AtomicBoolean _gossipServiceRunning; + + /** A ExecutorService used for executing the active and passive gossip threads. */ + private ExecutorService _gossipThreadExecutor; + + private Class _passiveGossipThreadClass; + + private Class _activeGossipThreadClass; + + public GossipManager(Class passiveGossipThreadClass, Class activeGossipThreadClass, String address, int port, GossipSettings settings, ArrayList gossipMembers) { + // Set the active and passive gossip thread classes to use. + _passiveGossipThreadClass = passiveGossipThreadClass; + _activeGossipThreadClass = activeGossipThreadClass; + + // Assign the GossipSettings to the instance variable. + _settings = settings; + + // Create the local gossip member which I am representing. + _me = new LocalGossipMember(address, port, 0, this, settings.getCleanupInterval()); + + // Initialize the gossip members list. + _memberList = new ArrayList(); + + // Initialize the dead gossip members list. + _deadList = new ArrayList(); + + // Print the startup member list when the service is in debug mode. + GossipService.debug("Startup member list:"); + GossipService.debug("---------------------"); + // First print out myself. + GossipService.debug(_me); + // Copy the list with members given to the local member list and print the member when in debug mode. + for (GossipMember startupMember : gossipMembers) { + if (!startupMember.equals(_me)) { + LocalGossipMember member = new LocalGossipMember(startupMember.getHost(), startupMember.getPort(), 0, this, settings.getCleanupInterval()); + _memberList.add(member); + GossipService.debug(member); + } else { + GossipService.info("Found myself in the members section of the configuration, you should not add the host itself to the members section."); + } + } + + // Set the boolean for running the gossip service to true. + _gossipServiceRunning = new AtomicBoolean(true); + + // Add a shutdown hook so we can see when the service has been shutdown. + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { + public void run() { + GossipService.info("Service has been shutdown..."); + } + })); + } + + /** + * All timers associated with a member will trigger this method when it goes + * off. The timer will go off if we have not heard from this member in + * _settings.T_CLEANUP time. + */ + @Override + public void handleNotification(Notification notification, Object handback) { + + // Get the local gossip member associated with the notification. + LocalGossipMember deadMember = (LocalGossipMember) notification.getUserData(); + + GossipService.info("Dead member detected: " + deadMember); + + // Remove the member from the active member list. + synchronized (this._memberList) { + this._memberList.remove(deadMember); + } + + // Add the member to the dead member list. + synchronized (this._deadList) { + this._deadList.add(deadMember); + } + } + + public GossipSettings getSettings() { + return _settings; + } + + /** + * Get a clone of the memberlist. + * @return + */ + public ArrayList getMemberList() { + return _memberList; + } + + public LocalGossipMember getMyself() { + return _me; + } + + public ArrayList getDeadList() { + return _deadList; + } + + /** + * Starts the client. Specifically, start the various cycles for this protocol. + * Start the gossip thread and start the receiver thread. + * @throws InterruptedException + */ + public void run() { + // Start all timers except for me + for (LocalGossipMember member : _memberList) { + if (member != _me) { + member.startTimeoutTimer(); + } + } + + try { + _gossipThreadExecutor = Executors.newCachedThreadPool(); + // The receiver thread is a passive player that handles + // merging incoming membership lists from other neighbors. + _gossipThreadExecutor.execute(_passiveGossipThreadClass.getConstructor(GossipManager.class).newInstance(this)); + // The gossiper thread is an active player that + // selects a neighbor to share its membership list + _gossipThreadExecutor.execute(_activeGossipThreadClass.getConstructor(GossipManager.class).newInstance(this)); + } catch (IllegalArgumentException e1) { + e1.printStackTrace(); + } catch (SecurityException e1) { + e1.printStackTrace(); + } catch (InstantiationException e1) { + e1.printStackTrace(); + } catch (IllegalAccessException e1) { + e1.printStackTrace(); + } catch (InvocationTargetException e1) { + e1.printStackTrace(); + } catch (NoSuchMethodException e1) { + e1.printStackTrace(); + } + + // Potentially, you could kick off more threads here + // that could perform additional data synching + + GossipService.info("The GossipService is started."); + + // keep the main thread around + while(_gossipServiceRunning.get()) { + try { + TimeUnit.SECONDS.sleep(10); + } catch (InterruptedException e) { + GossipService.info("The GossipClient was interrupted."); + } + } + } + + /** + * Shutdown the gossip service. + */ + public void shutdown() { + _gossipThreadExecutor.shutdown(); + _gossipServiceRunning.set(false); + } +} diff --git a/src/main/java/com/google/code/gossip/manager/PassiveGossipThread.java b/src/main/java/com/google/code/gossip/manager/PassiveGossipThread.java new file mode 100644 index 0000000..40c33bf --- /dev/null +++ b/src/main/java/com/google/code/gossip/manager/PassiveGossipThread.java @@ -0,0 +1,145 @@ +package com.google.code.gossip.manager; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import com.google.code.gossip.GossipMember; +import com.google.code.gossip.GossipService; +import com.google.code.gossip.RemoteGossipMember; + +/** + * [The passive thread: reply to incoming gossip request.] + * This class handles the passive cycle, where this client + * has received an incoming message. For now, this message + * is always the membership list, but if you choose to gossip + * additional information, you will need some logic to determine + * the incoming message. + */ +abstract public class PassiveGossipThread implements Runnable { + + /** The socket used for the passive thread of the gossip service. */ + private DatagramSocket _server; + + private GossipManager _gossipManager; + + private AtomicBoolean _keepRunning; + + public PassiveGossipThread(GossipManager gossipManager) { + _gossipManager = gossipManager; + + // Start the service on the given port number. + try { + _server = new DatagramSocket(_gossipManager.getMyself().getPort()); + + // The server successfully started on the current port. + GossipService.info("Gossip service successfully initialized on port " + _gossipManager.getMyself().getPort()); + GossipService.debug("I am " + _gossipManager.getMyself()); + } catch (SocketException ex) { + // The port is probably already in use. + _server = null; + // Let's communicate this to the user. + GossipService.error("Error while starting the gossip service on port " + _gossipManager.getMyself().getPort() + ": " + ex.getMessage()); + System.exit(-1); + } + + _keepRunning = new AtomicBoolean(true); + } + + @Override + public void run() { + while(_keepRunning.get()) { + try { + // Create a byte array with the size of the buffer. + byte[] buf = new byte[_server.getReceiveBufferSize()]; + DatagramPacket p = new DatagramPacket(buf, buf.length); + _server.receive(p); + GossipService.debug("A message has been received from " + p.getAddress() + ":" + p.getPort() + "."); + + int packet_length = 0; + for (int i = 0; i < 4; i++) { + int shift = (4 - 1 - i) * 8; + packet_length += (buf[i] & 0x000000FF) << shift; + } + + // Check whether the package is smaller than the maximal packet length. + // A package larger than this would not be possible to be send from a GossipService, + // since this is check before sending the message. + // This could normally only occur when the list of members is very big, + // or when the packet is misformed, and the first 4 bytes is not the right in anymore. + // For this reason we regards the message. + if (packet_length <= GossipManager.MAX_PACKET_SIZE) { + + byte[] json_bytes = new byte[packet_length]; + for (int i=0; i remoteGossipMembers = new ArrayList(); + + RemoteGossipMember senderMember = null; + + GossipService.debug("Received member list:"); + // Convert the received JSON message to a JSON array. + JSONArray jsonArray = new JSONArray(receivedMessage); + // The JSON array should contain all members. + // Let's iterate over them. + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject memberJSONObject = jsonArray.getJSONObject(i); + // Now the array should contain 3 objects (hostname, port and heartbeat). + if (memberJSONObject.length() == 3) { + // Ok, now let's create the member object. + RemoteGossipMember member = new RemoteGossipMember(memberJSONObject.getString(GossipMember.JSON_HOST), memberJSONObject.getInt(GossipMember.JSON_PORT), memberJSONObject.getInt(GossipMember.JSON_HEARTBEAT)); + GossipService.debug(member.toString()); + + // This is the first member found, so this should be the member who is communicating with me. + if (i == 0) { + senderMember = member; + } + + remoteGossipMembers.add(member); + } else { + GossipService.error("The received member object does not contain 3 objects:\n" + memberJSONObject.toString()); + } + + } + + // Merge our list with the one we just received + mergeLists(_gossipManager, senderMember, remoteGossipMembers); + + } catch (JSONException e) { + GossipService.error("The received message is not well-formed JSON. The following message has been dropped:\n" + receivedMessage); + } + + } else { + GossipService.error("The received message is not of the expected size, it has been dropped."); + } + + } catch (IOException e) { + e.printStackTrace(); + _keepRunning.set(false); + } + } + } + + /** + * Abstract method for merging the local and remote list. + * @param gossipManager The GossipManager for retrieving the local members and dead members list. + * @param senderMember The member who is sending this list, this could be used to send a response if the remote list contains out-dated information. + * @param remoteList The list of members known at the remote side. + */ + abstract protected void mergeLists(GossipManager gossipManager, RemoteGossipMember senderMember, ArrayList remoteList); +} \ No newline at end of file diff --git a/src/main/java/com/google/code/gossip/manager/impl/OnlyProcessReceivedPassiveGossipThread.java b/src/main/java/com/google/code/gossip/manager/impl/OnlyProcessReceivedPassiveGossipThread.java new file mode 100644 index 0000000..271e119 --- /dev/null +++ b/src/main/java/com/google/code/gossip/manager/impl/OnlyProcessReceivedPassiveGossipThread.java @@ -0,0 +1,93 @@ +package com.google.code.gossip.manager.impl; + +import java.util.ArrayList; + +import com.google.code.gossip.GossipMember; +import com.google.code.gossip.GossipService; +import com.google.code.gossip.LocalGossipMember; +import com.google.code.gossip.RemoteGossipMember; +import com.google.code.gossip.manager.GossipManager; +import com.google.code.gossip.manager.PassiveGossipThread; + +public class OnlyProcessReceivedPassiveGossipThread extends PassiveGossipThread { + + public OnlyProcessReceivedPassiveGossipThread(GossipManager gossipManager) { + super(gossipManager); + } + + /** + * Merge remote list (received from peer), and our local member list. + * Simply, we must update the heartbeats that the remote list has with + * our list. Also, some additional logic is needed to make sure we have + * not timed out a member and then immediately received a list with that + * member. + * @param remoteList + */ + protected void mergeLists(GossipManager gossipManager, RemoteGossipMember senderMember, ArrayList remoteList) { + + synchronized (gossipManager.getDeadList()) { + + synchronized (gossipManager.getMemberList()) { + + for (GossipMember remoteMember : remoteList) { + // Skip myself. We don't want ourselves in the local member list. + if (!remoteMember.equals(gossipManager.getMyself())) { + if (gossipManager.getMemberList().contains(remoteMember)) { + GossipService.debug("The local list already contains the remote member (" + remoteMember + ")."); + // The local memberlist contains the remote member. + LocalGossipMember localMember = gossipManager.getMemberList().get(gossipManager.getMemberList().indexOf(remoteMember)); + + // Let's synchronize it's heartbeat. + if (remoteMember.getHeartbeat() > localMember.getHeartbeat()) { + // update local list with latest heartbeat + localMember.setHeartbeat(remoteMember.getHeartbeat()); + // and reset the timeout of that member + localMember.resetTimeoutTimer(); + } + // TODO: Otherwise, should we inform the other when the heartbeat is already higher? + } else { + // The local list does not contain the remote member. + GossipService.debug("The local list does not contain the remote member (" + remoteMember + ")."); + + // The remote member is either brand new, or a previously declared dead member. + // If its dead, check the heartbeat because it may have come back from the dead. + if (gossipManager.getDeadList().contains(remoteMember)) { + // The remote member is known here as a dead member. + GossipService.debug("The remote member is known here as a dead member."); + LocalGossipMember localDeadMember = gossipManager.getDeadList().get(gossipManager.getDeadList().indexOf(remoteMember)); + // If a member is restarted the heartbeat will restart from 1, so we should check that here. + // So a member can become from the dead when it is either larger than a previous heartbeat (due to network failure) + // or when the heartbeat is 1 (after a restart of the service). + // TODO: What if the first message of a gossip service is sent to a dead node? The second member will receive a heartbeat of two. + // TODO: The above does happen. Maybe a special message for a revived member? + // TODO: Or maybe when a member is declared dead for more than _settings.getCleanupInterval() ms, reset the heartbeat to 0. + // It will then accept a revived member. + // The above is now handle by checking whether the heartbeat differs _settings.getCleanupInterval(), it must be restarted. + if (remoteMember.getHeartbeat() == 1 + || ((localDeadMember.getHeartbeat() - remoteMember.getHeartbeat()) * -1) > (gossipManager.getSettings().getCleanupInterval() / 1000) + || remoteMember.getHeartbeat() > localDeadMember.getHeartbeat()) { + GossipService.debug("The remote member is back from the dead. We will remove it from the dead list and add it as a new member."); + // The remote member is back from the dead. + // Remove it from the dead list. + gossipManager.getDeadList().remove(localDeadMember); + // Add it as a new member and add it to the member list. + LocalGossipMember newLocalMember = new LocalGossipMember(remoteMember.getHost(), remoteMember.getPort(), remoteMember.getHeartbeat(), gossipManager, gossipManager.getSettings().getCleanupInterval()); + gossipManager.getMemberList().add(newLocalMember); + newLocalMember.startTimeoutTimer(); + GossipService.info("Removed remote member " + remoteMember.getAddress() + " from dead list and added to local member list."); + } + } else { + // Brand spanking new member - welcome. + LocalGossipMember newLocalMember = new LocalGossipMember(remoteMember.getHost(), remoteMember.getPort(), remoteMember.getHeartbeat(), gossipManager, gossipManager.getSettings().getCleanupInterval()); + gossipManager.getMemberList().add(newLocalMember); + newLocalMember.startTimeoutTimer(); + GossipService.info("Added new remote member " + remoteMember.getAddress() + " to local member list."); + } + } + } + } + } + } + } + +} diff --git a/src/main/java/com/google/code/gossip/manager/impl/SendMembersActiveGossipThread.java b/src/main/java/com/google/code/gossip/manager/impl/SendMembersActiveGossipThread.java new file mode 100644 index 0000000..daf10e3 --- /dev/null +++ b/src/main/java/com/google/code/gossip/manager/impl/SendMembersActiveGossipThread.java @@ -0,0 +1,106 @@ +package com.google.code.gossip.manager.impl; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.util.ArrayList; + +import org.json.JSONArray; + +import com.google.code.gossip.GossipService; +import com.google.code.gossip.LocalGossipMember; +import com.google.code.gossip.manager.ActiveGossipThread; +import com.google.code.gossip.manager.GossipManager; +import com.sun.xml.internal.ws.util.ByteArrayBuffer; + +abstract public class SendMembersActiveGossipThread extends ActiveGossipThread { + + public SendMembersActiveGossipThread(GossipManager gossipManager) { + super(gossipManager); + } + + /** + * Performs the sending of the membership list, after we have + * incremented our own heartbeat. + */ + protected void sendMembershipList(LocalGossipMember me, ArrayList memberList) { + GossipService.debug("Send sendMembershipList() is called."); + + // Increase the heartbeat of myself by 1. + me.setHeartbeat(me.getHeartbeat() + 1); + + synchronized (memberList) { + try { + LocalGossipMember member = selectPartner(memberList); + + if (member != null) { + InetAddress dest = InetAddress.getByName(member.getHost()); + + // Create a StringBuffer for the JSON message. + JSONArray jsonArray = new JSONArray(); + GossipService.debug("Sending memberlist to " + dest + ":" + member.getPort()); + GossipService.debug("---------------------"); + + // First write myself, append the JSON representation of the member to the buffer. + jsonArray.put(me.toJSONObject()); + GossipService.debug(me); + + // Then write the others. + for (int i=0; i> 24 ); + length_bytes[1] =(byte)( (packet_length << 8) >> 24 ); + length_bytes[2] =(byte)( (packet_length << 16) >> 24 ); + length_bytes[3] =(byte)( (packet_length << 24) >> 24 ); + + + GossipService.debug("Sending message ("+packet_length+" bytes): " + jsonArray.toString()); + + + ByteArrayBuffer byteBuffer = new ByteArrayBuffer(); + // Write the first 4 bytes with the length of the rest of the packet. + byteBuffer.write(length_bytes); + // Write the json data. + byteBuffer.write(json_bytes); + + byte[] buf = byteBuffer.getRawData(); + + /* + ByteBuffer byteBuffer = ByteBuffer.allocate(4 + json_bytes.length); + byteBuffer.put(length_bytes); + byteBuffer.put(json_bytes); + byte[] buf = byteBuffer.array(); + */ + + DatagramSocket socket = new DatagramSocket(); + DatagramPacket datagramPacket = new DatagramPacket(buf, buf.length, dest, member.getPort()); + socket.send(datagramPacket); + socket.close(); + } else { + GossipService.error("The length of the to be send message is too large (" + packet_length + " > " + GossipManager.MAX_PACKET_SIZE + ")."); + } + } + + } catch (IOException e1) { + e1.printStackTrace(); + } + } + } +} diff --git a/src/main/java/com/google/code/gossip/manager/random/RandomActiveGossipThread.java b/src/main/java/com/google/code/gossip/manager/random/RandomActiveGossipThread.java new file mode 100644 index 0000000..6986156 --- /dev/null +++ b/src/main/java/com/google/code/gossip/manager/random/RandomActiveGossipThread.java @@ -0,0 +1,44 @@ +package com.google.code.gossip.manager.random; + +import java.util.ArrayList; +import java.util.Random; + +import com.google.code.gossip.GossipService; +import com.google.code.gossip.LocalGossipMember; +import com.google.code.gossip.manager.GossipManager; +import com.google.code.gossip.manager.impl.SendMembersActiveGossipThread; + +public class RandomActiveGossipThread extends SendMembersActiveGossipThread { + + /** The Random used for choosing a member to gossip with. */ + private Random _random; + + public RandomActiveGossipThread(GossipManager gossipManager) { + super(gossipManager); + + // Initialize the random used for deciding on which gossip member to gossip with. + _random = new Random(); + } + + /** + * [The selectToSend() function.] + * Find a random peer from the local membership list. + * In the case where this client is the only member in the list, this method will return null. + * @return Member random member if list is greater than 1, null otherwise + */ + protected LocalGossipMember selectPartner(ArrayList memberList) { + LocalGossipMember member = null; + + // We can only send a message if there are actually other members. + if (memberList.size() > 0) { + // Get the index of the random member. + int randomNeighborIndex = _random.nextInt(memberList.size()); + member = memberList.get(randomNeighborIndex); + } else { + GossipService.debug("I am alone in this world."); + } + + return member; + } + +} diff --git a/src/main/java/com/google/code/gossip/manager/random/RandomGossipManager.java b/src/main/java/com/google/code/gossip/manager/random/RandomGossipManager.java new file mode 100644 index 0000000..70e18a0 --- /dev/null +++ b/src/main/java/com/google/code/gossip/manager/random/RandomGossipManager.java @@ -0,0 +1,14 @@ +package com.google.code.gossip.manager.random; + +import java.util.ArrayList; + +import com.google.code.gossip.GossipMember; +import com.google.code.gossip.GossipSettings; +import com.google.code.gossip.manager.GossipManager; +import com.google.code.gossip.manager.impl.OnlyProcessReceivedPassiveGossipThread; + +public class RandomGossipManager extends GossipManager { + public RandomGossipManager(String address, int port, GossipSettings settings, ArrayList gossipMembers) { + super(OnlyProcessReceivedPassiveGossipThread.class, RandomActiveGossipThread.class, address, port, settings, gossipMembers); + } +} diff --git a/src/main/java/org/json/CDL.java b/src/main/java/org/json/CDL.java new file mode 100644 index 0000000..d76dd68 --- /dev/null +++ b/src/main/java/org/json/CDL.java @@ -0,0 +1,279 @@ +package org.json; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * This provides static methods to convert comma delimited text into a + * JSONArray, and to covert a JSONArray into comma delimited text. Comma + * delimited text is a very popular format for data interchange. It is + * understood by most database, spreadsheet, and organizer programs. + *

+ * Each row of text represents a row in a table or a data record. Each row + * ends with a NEWLINE character. Each row contains one or more values. + * Values are separated by commas. A value can contain any character except + * for comma, unless is is wrapped in single quotes or double quotes. + *

+ * The first row usually contains the names of the columns. + *

+ * A comma delimited list can be converted into a JSONArray of JSONObjects. + * The names for the elements in the JSONObjects can be taken from the names + * in the first row. + * @author JSON.org + * @version 2009-09-11 + */ +public class CDL { + + /** + * Get the next value. The value can be wrapped in quotes. The value can + * be empty. + * @param x A JSONTokener of the source text. + * @return The value string, or null if empty. + * @throws JSONException if the quoted string is badly formed. + */ + private static String getValue(JSONTokener x) throws JSONException { + char c; + char q; + StringBuffer sb; + do { + c = x.next(); + } while (c == ' ' || c == '\t'); + switch (c) { + case 0: + return null; + case '"': + case '\'': + q = c; + sb = new StringBuffer(); + for (;;) { + c = x.next(); + if (c == q) { + break; + } + if (c == 0 || c == '\n' || c == '\r') { + throw x.syntaxError("Missing close quote '" + q + "'."); + } + sb.append(c); + } + return sb.toString(); + case ',': + x.back(); + return ""; + default: + x.back(); + return x.nextTo(','); + } + } + + /** + * Produce a JSONArray of strings from a row of comma delimited values. + * @param x A JSONTokener of the source text. + * @return A JSONArray of strings. + * @throws JSONException + */ + public static JSONArray rowToJSONArray(JSONTokener x) throws JSONException { + JSONArray ja = new JSONArray(); + for (;;) { + String value = getValue(x); + char c = x.next(); + if (value == null || + (ja.length() == 0 && value.length() == 0 && c != ',')) { + return null; + } + ja.put(value); + for (;;) { + if (c == ',') { + break; + } + if (c != ' ') { + if (c == '\n' || c == '\r' || c == 0) { + return ja; + } + throw x.syntaxError("Bad character '" + c + "' (" + + (int)c + ")."); + } + c = x.next(); + } + } + } + + /** + * Produce a JSONObject from a row of comma delimited text, using a + * parallel JSONArray of strings to provides the names of the elements. + * @param names A JSONArray of names. This is commonly obtained from the + * first row of a comma delimited text file using the rowToJSONArray + * method. + * @param x A JSONTokener of the source text. + * @return A JSONObject combining the names and values. + * @throws JSONException + */ + public static JSONObject rowToJSONObject(JSONArray names, JSONTokener x) + throws JSONException { + JSONArray ja = rowToJSONArray(x); + return ja != null ? ja.toJSONObject(names) : null; + } + + /** + * Produce a JSONArray of JSONObjects from a comma delimited text string, + * using the first row as a source of names. + * @param string The comma delimited text. + * @return A JSONArray of JSONObjects. + * @throws JSONException + */ + public static JSONArray toJSONArray(String string) throws JSONException { + return toJSONArray(new JSONTokener(string)); + } + + /** + * Produce a JSONArray of JSONObjects from a comma delimited text string, + * using the first row as a source of names. + * @param x The JSONTokener containing the comma delimited text. + * @return A JSONArray of JSONObjects. + * @throws JSONException + */ + public static JSONArray toJSONArray(JSONTokener x) throws JSONException { + return toJSONArray(rowToJSONArray(x), x); + } + + /** + * Produce a JSONArray of JSONObjects from a comma delimited text string + * using a supplied JSONArray as the source of element names. + * @param names A JSONArray of strings. + * @param string The comma delimited text. + * @return A JSONArray of JSONObjects. + * @throws JSONException + */ + public static JSONArray toJSONArray(JSONArray names, String string) + throws JSONException { + return toJSONArray(names, new JSONTokener(string)); + } + + /** + * Produce a JSONArray of JSONObjects from a comma delimited text string + * using a supplied JSONArray as the source of element names. + * @param names A JSONArray of strings. + * @param x A JSONTokener of the source text. + * @return A JSONArray of JSONObjects. + * @throws JSONException + */ + public static JSONArray toJSONArray(JSONArray names, JSONTokener x) + throws JSONException { + if (names == null || names.length() == 0) { + return null; + } + JSONArray ja = new JSONArray(); + for (;;) { + JSONObject jo = rowToJSONObject(names, x); + if (jo == null) { + break; + } + ja.put(jo); + } + if (ja.length() == 0) { + return null; + } + return ja; + } + + + /** + * Produce a comma delimited text row from a JSONArray. Values containing + * the comma character will be quoted. Troublesome characters may be + * removed. + * @param ja A JSONArray of strings. + * @return A string ending in NEWLINE. + */ + public static String rowToString(JSONArray ja) { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < ja.length(); i += 1) { + if (i > 0) { + sb.append(','); + } + Object o = ja.opt(i); + if (o != null) { + String s = o.toString(); + if (s.length() > 0 && (s.indexOf(',') >= 0 || s.indexOf('\n') >= 0 || + s.indexOf('\r') >= 0 || s.indexOf(0) >= 0 || + s.charAt(0) == '"')) { + sb.append('"'); + int length = s.length(); + for (int j = 0; j < length; j += 1) { + char c = s.charAt(j); + if (c >= ' ' && c != '"') { + sb.append(c); + } + } + sb.append('"'); + } else { + sb.append(s); + } + } + } + sb.append('\n'); + return sb.toString(); + } + + /** + * Produce a comma delimited text from a JSONArray of JSONObjects. The + * first row will be a list of names obtained by inspecting the first + * JSONObject. + * @param ja A JSONArray of JSONObjects. + * @return A comma delimited text. + * @throws JSONException + */ + public static String toString(JSONArray ja) throws JSONException { + JSONObject jo = ja.optJSONObject(0); + if (jo != null) { + JSONArray names = jo.names(); + if (names != null) { + return rowToString(names) + toString(names, ja); + } + } + return null; + } + + /** + * Produce a comma delimited text from a JSONArray of JSONObjects using + * a provided list of names. The list of names is not included in the + * output. + * @param names A JSONArray of strings. + * @param ja A JSONArray of JSONObjects. + * @return A comma delimited text. + * @throws JSONException + */ + public static String toString(JSONArray names, JSONArray ja) + throws JSONException { + if (names == null || names.length() == 0) { + return null; + } + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < ja.length(); i += 1) { + JSONObject jo = ja.optJSONObject(i); + if (jo != null) { + sb.append(rowToString(jo.toJSONArray(names))); + } + } + return sb.toString(); + } +} diff --git a/src/main/java/org/json/JSONArray.java b/src/main/java/org/json/JSONArray.java new file mode 100644 index 0000000..5cc6f71 --- /dev/null +++ b/src/main/java/org/json/JSONArray.java @@ -0,0 +1,918 @@ +package org.json; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import java.io.IOException; +import java.io.Writer; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; + +/** + * A JSONArray is an ordered sequence of values. Its external text form is a + * string wrapped in square brackets with commas separating the values. The + * internal form is an object having get and opt + * methods for accessing the values by index, and put methods for + * adding or replacing values. The values can be any of these types: + * Boolean, JSONArray, JSONObject, + * Number, String, or the + * JSONObject.NULL object. + *

+ * The constructor can convert a JSON text into a Java object. The + * toString method converts to JSON text. + *

+ * A get method returns a value if one can be found, and throws an + * exception if one cannot be found. An opt method returns a + * default value instead of throwing an exception, and so is useful for + * obtaining optional values. + *

+ * The generic get() and opt() methods return an + * object which you can cast or query for type. There are also typed + * get and opt methods that do type checking and type + * coercion for you. + *

+ * The texts produced by the toString methods strictly conform to + * JSON syntax rules. The constructors are more forgiving in the texts they will + * accept: + *

    + *
  • An extra , (comma) may appear just + * before the closing bracket.
  • + *
  • The null value will be inserted when there + * is , (comma) elision.
  • + *
  • Strings may be quoted with ' (single + * quote).
  • + *
  • Strings do not need to be quoted at all if they do not begin with a quote + * or single quote, and if they do not contain leading or trailing spaces, + * and if they do not contain any of these characters: + * { } [ ] / \ : , = ; # and if they do not look like numbers + * and if they are not the reserved words true, + * false, or null.
  • + *
  • Values can be separated by ; (semicolon) as + * well as by , (comma).
  • + *
  • Numbers may have the + * 0x- (hex) prefix.
  • + *
+ + * @author JSON.org + * @version 2009-04-14 + */ +public class JSONArray { + + + /** + * The arrayList where the JSONArray's properties are kept. + */ + private ArrayList myArrayList; + + + /** + * Construct an empty JSONArray. + */ + public JSONArray() { + this.myArrayList = new ArrayList(); + } + + /** + * Construct a JSONArray from a JSONTokener. + * @param x A JSONTokener + * @throws JSONException If there is a syntax error. + */ + public JSONArray(JSONTokener x) throws JSONException { + this(); + char c = x.nextClean(); + char q; + if (c == '[') { + q = ']'; + } else if (c == '(') { + q = ')'; + } else { + throw x.syntaxError("A JSONArray text must start with '['"); + } + if (x.nextClean() == ']') { + return; + } + x.back(); + for (;;) { + if (x.nextClean() == ',') { + x.back(); + this.myArrayList.add(null); + } else { + x.back(); + this.myArrayList.add(x.nextValue()); + } + c = x.nextClean(); + switch (c) { + case ';': + case ',': + if (x.nextClean() == ']') { + return; + } + x.back(); + break; + case ']': + case ')': + if (q != c) { + throw x.syntaxError("Expected a '" + new Character(q) + "'"); + } + return; + default: + throw x.syntaxError("Expected a ',' or ']'"); + } + } + } + + + /** + * Construct a JSONArray from a source JSON text. + * @param source A string that begins with + * [ (left bracket) + * and ends with ] (right bracket). + * @throws JSONException If there is a syntax error. + */ + public JSONArray(String source) throws JSONException { + this(new JSONTokener(source)); + } + + + /** + * Construct a JSONArray from a Collection. + * @param collection A Collection. + */ + public JSONArray(Collection collection) { + this.myArrayList = new ArrayList(); + if (collection != null) { + Iterator iter = collection.iterator(); + while (iter.hasNext()) { + Object o = iter.next(); + this.myArrayList.add(JSONObject.wrap(o)); + } + } + } + + + /** + * Construct a JSONArray from an array + * @throws JSONException If not an array. + */ + public JSONArray(Object array) throws JSONException { + this(); + if (array.getClass().isArray()) { + int length = Array.getLength(array); + for (int i = 0; i < length; i += 1) { + this.put(JSONObject.wrap(Array.get(array, i))); + } + } else { + throw new JSONException( +"JSONArray initial value should be a string or collection or array."); + } + } + + + /** + * Get the object value associated with an index. + * @param index + * The index must be between 0 and length() - 1. + * @return An object value. + * @throws JSONException If there is no value for the index. + */ + public Object get(int index) throws JSONException { + Object o = opt(index); + if (o == null) { + throw new JSONException("JSONArray[" + index + "] not found."); + } + return o; + } + + + /** + * Get the boolean value associated with an index. + * The string values "true" and "false" are converted to boolean. + * + * @param index The index must be between 0 and length() - 1. + * @return The truth. + * @throws JSONException If there is no value for the index or if the + * value is not convertable to boolean. + */ + public boolean getBoolean(int index) throws JSONException { + Object o = get(index); + if (o.equals(Boolean.FALSE) || + (o instanceof String && + ((String)o).equalsIgnoreCase("false"))) { + return false; + } else if (o.equals(Boolean.TRUE) || + (o instanceof String && + ((String)o).equalsIgnoreCase("true"))) { + return true; + } + throw new JSONException("JSONArray[" + index + "] is not a Boolean."); + } + + + /** + * Get the double value associated with an index. + * + * @param index The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException If the key is not found or if the value cannot + * be converted to a number. + */ + public double getDouble(int index) throws JSONException { + Object o = get(index); + try { + return o instanceof Number ? + ((Number)o).doubleValue() : + Double.valueOf((String)o).doubleValue(); + } catch (Exception e) { + throw new JSONException("JSONArray[" + index + + "] is not a number."); + } + } + + + /** + * Get the int value associated with an index. + * + * @param index The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException If the key is not found or if the value cannot + * be converted to a number. + * if the value cannot be converted to a number. + */ + public int getInt(int index) throws JSONException { + Object o = get(index); + return o instanceof Number ? + ((Number)o).intValue() : (int)getDouble(index); + } + + + /** + * Get the JSONArray associated with an index. + * @param index The index must be between 0 and length() - 1. + * @return A JSONArray value. + * @throws JSONException If there is no value for the index. or if the + * value is not a JSONArray + */ + public JSONArray getJSONArray(int index) throws JSONException { + Object o = get(index); + if (o instanceof JSONArray) { + return (JSONArray)o; + } + throw new JSONException("JSONArray[" + index + + "] is not a JSONArray."); + } + + + /** + * Get the JSONObject associated with an index. + * @param index subscript + * @return A JSONObject value. + * @throws JSONException If there is no value for the index or if the + * value is not a JSONObject + */ + public JSONObject getJSONObject(int index) throws JSONException { + Object o = get(index); + if (o instanceof JSONObject) { + return (JSONObject)o; + } + throw new JSONException("JSONArray[" + index + + "] is not a JSONObject."); + } + + + /** + * Get the long value associated with an index. + * + * @param index The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException If the key is not found or if the value cannot + * be converted to a number. + */ + public long getLong(int index) throws JSONException { + Object o = get(index); + return o instanceof Number ? + ((Number)o).longValue() : (long)getDouble(index); + } + + + /** + * Get the string associated with an index. + * @param index The index must be between 0 and length() - 1. + * @return A string value. + * @throws JSONException If there is no value for the index. + */ + public String getString(int index) throws JSONException { + return get(index).toString(); + } + + + /** + * Determine if the value is null. + * @param index The index must be between 0 and length() - 1. + * @return true if the value at the index is null, or if there is no value. + */ + public boolean isNull(int index) { + return JSONObject.NULL.equals(opt(index)); + } + + + /** + * Make a string from the contents of this JSONArray. The + * separator string is inserted between each element. + * Warning: This method assumes that the data structure is acyclical. + * @param separator A string that will be inserted between the elements. + * @return a string. + * @throws JSONException If the array contains an invalid number. + */ + public String join(String separator) throws JSONException { + int len = length(); + StringBuffer sb = new StringBuffer(); + + for (int i = 0; i < len; i += 1) { + if (i > 0) { + sb.append(separator); + } + sb.append(JSONObject.valueToString(this.myArrayList.get(i))); + } + return sb.toString(); + } + + + /** + * Get the number of elements in the JSONArray, included nulls. + * + * @return The length (or size). + */ + public int length() { + return this.myArrayList.size(); + } + + + /** + * Get the optional object value associated with an index. + * @param index The index must be between 0 and length() - 1. + * @return An object value, or null if there is no + * object at that index. + */ + public Object opt(int index) { + return (index < 0 || index >= length()) ? + null : this.myArrayList.get(index); + } + + + /** + * Get the optional boolean value associated with an index. + * It returns false if there is no value at that index, + * or if the value is not Boolean.TRUE or the String "true". + * + * @param index The index must be between 0 and length() - 1. + * @return The truth. + */ + public boolean optBoolean(int index) { + return optBoolean(index, false); + } + + + /** + * Get the optional boolean value associated with an index. + * It returns the defaultValue if there is no value at that index or if + * it is not a Boolean or the String "true" or "false" (case insensitive). + * + * @param index The index must be between 0 and length() - 1. + * @param defaultValue A boolean default. + * @return The truth. + */ + public boolean optBoolean(int index, boolean defaultValue) { + try { + return getBoolean(index); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get the optional double value associated with an index. + * NaN is returned if there is no value for the index, + * or if the value is not a number and cannot be converted to a number. + * + * @param index The index must be between 0 and length() - 1. + * @return The value. + */ + public double optDouble(int index) { + return optDouble(index, Double.NaN); + } + + + /** + * Get the optional double value associated with an index. + * The defaultValue is returned if there is no value for the index, + * or if the value is not a number and cannot be converted to a number. + * + * @param index subscript + * @param defaultValue The default value. + * @return The value. + */ + public double optDouble(int index, double defaultValue) { + try { + return getDouble(index); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get the optional int value associated with an index. + * Zero is returned if there is no value for the index, + * or if the value is not a number and cannot be converted to a number. + * + * @param index The index must be between 0 and length() - 1. + * @return The value. + */ + public int optInt(int index) { + return optInt(index, 0); + } + + + /** + * Get the optional int value associated with an index. + * The defaultValue is returned if there is no value for the index, + * or if the value is not a number and cannot be converted to a number. + * @param index The index must be between 0 and length() - 1. + * @param defaultValue The default value. + * @return The value. + */ + public int optInt(int index, int defaultValue) { + try { + return getInt(index); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get the optional JSONArray associated with an index. + * @param index subscript + * @return A JSONArray value, or null if the index has no value, + * or if the value is not a JSONArray. + */ + public JSONArray optJSONArray(int index) { + Object o = opt(index); + return o instanceof JSONArray ? (JSONArray)o : null; + } + + + /** + * Get the optional JSONObject associated with an index. + * Null is returned if the key is not found, or null if the index has + * no value, or if the value is not a JSONObject. + * + * @param index The index must be between 0 and length() - 1. + * @return A JSONObject value. + */ + public JSONObject optJSONObject(int index) { + Object o = opt(index); + return o instanceof JSONObject ? (JSONObject)o : null; + } + + + /** + * Get the optional long value associated with an index. + * Zero is returned if there is no value for the index, + * or if the value is not a number and cannot be converted to a number. + * + * @param index The index must be between 0 and length() - 1. + * @return The value. + */ + public long optLong(int index) { + return optLong(index, 0); + } + + + /** + * Get the optional long value associated with an index. + * The defaultValue is returned if there is no value for the index, + * or if the value is not a number and cannot be converted to a number. + * @param index The index must be between 0 and length() - 1. + * @param defaultValue The default value. + * @return The value. + */ + public long optLong(int index, long defaultValue) { + try { + return getLong(index); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get the optional string value associated with an index. It returns an + * empty string if there is no value at that index. If the value + * is not a string and is not null, then it is coverted to a string. + * + * @param index The index must be between 0 and length() - 1. + * @return A String value. + */ + public String optString(int index) { + return optString(index, ""); + } + + + /** + * Get the optional string associated with an index. + * The defaultValue is returned if the key is not found. + * + * @param index The index must be between 0 and length() - 1. + * @param defaultValue The default value. + * @return A String value. + */ + public String optString(int index, String defaultValue) { + Object o = opt(index); + return o != null ? o.toString() : defaultValue; + } + + + /** + * Append a boolean value. This increases the array's length by one. + * + * @param value A boolean value. + * @return this. + */ + public JSONArray put(boolean value) { + put(value ? Boolean.TRUE : Boolean.FALSE); + return this; + } + + + /** + * Put a value in the JSONArray, where the value will be a + * JSONArray which is produced from a Collection. + * @param value A Collection value. + * @return this. + */ + public JSONArray put(Collection value) { + put(new JSONArray(value)); + return this; + } + + + /** + * Append a double value. This increases the array's length by one. + * + * @param value A double value. + * @throws JSONException if the value is not finite. + * @return this. + */ + public JSONArray put(double value) throws JSONException { + Double d = new Double(value); + JSONObject.testValidity(d); + put(d); + return this; + } + + + /** + * Append an int value. This increases the array's length by one. + * + * @param value An int value. + * @return this. + */ + public JSONArray put(int value) { + put(new Integer(value)); + return this; + } + + + /** + * Append an long value. This increases the array's length by one. + * + * @param value A long value. + * @return this. + */ + public JSONArray put(long value) { + put(new Long(value)); + return this; + } + + + /** + * Put a value in the JSONArray, where the value will be a + * JSONObject which is produced from a Map. + * @param value A Map value. + * @return this. + */ + public JSONArray put(Map value) { + put(new JSONObject(value)); + return this; + } + + + /** + * Append an object value. This increases the array's length by one. + * @param value An object value. The value should be a + * Boolean, Double, Integer, JSONArray, JSONObject, Long, or String, or the + * JSONObject.NULL object. + * @return this. + */ + public JSONArray put(Object value) { + this.myArrayList.add(value); + return this; + } + + + /** + * Put or replace a boolean value in the JSONArray. If the index is greater + * than the length of the JSONArray, then null elements will be added as + * necessary to pad it out. + * @param index The subscript. + * @param value A boolean value. + * @return this. + * @throws JSONException If the index is negative. + */ + public JSONArray put(int index, boolean value) throws JSONException { + put(index, value ? Boolean.TRUE : Boolean.FALSE); + return this; + } + + + /** + * Put a value in the JSONArray, where the value will be a + * JSONArray which is produced from a Collection. + * @param index The subscript. + * @param value A Collection value. + * @return this. + * @throws JSONException If the index is negative or if the value is + * not finite. + */ + public JSONArray put(int index, Collection value) throws JSONException { + put(index, new JSONArray(value)); + return this; + } + + + /** + * Put or replace a double value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad + * it out. + * @param index The subscript. + * @param value A double value. + * @return this. + * @throws JSONException If the index is negative or if the value is + * not finite. + */ + public JSONArray put(int index, double value) throws JSONException { + put(index, new Double(value)); + return this; + } + + + /** + * Put or replace an int value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad + * it out. + * @param index The subscript. + * @param value An int value. + * @return this. + * @throws JSONException If the index is negative. + */ + public JSONArray put(int index, int value) throws JSONException { + put(index, new Integer(value)); + return this; + } + + + /** + * Put or replace a long value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad + * it out. + * @param index The subscript. + * @param value A long value. + * @return this. + * @throws JSONException If the index is negative. + */ + public JSONArray put(int index, long value) throws JSONException { + put(index, new Long(value)); + return this; + } + + + /** + * Put a value in the JSONArray, where the value will be a + * JSONObject which is produced from a Map. + * @param index The subscript. + * @param value The Map value. + * @return this. + * @throws JSONException If the index is negative or if the the value is + * an invalid number. + */ + public JSONArray put(int index, Map value) throws JSONException { + put(index, new JSONObject(value)); + return this; + } + + + /** + * Put or replace an object value in the JSONArray. If the index is greater + * than the length of the JSONArray, then null elements will be added as + * necessary to pad it out. + * @param index The subscript. + * @param value The value to put into the array. The value should be a + * Boolean, Double, Integer, JSONArray, JSONObject, Long, or String, or the + * JSONObject.NULL object. + * @return this. + * @throws JSONException If the index is negative or if the the value is + * an invalid number. + */ + public JSONArray put(int index, Object value) throws JSONException { + JSONObject.testValidity(value); + if (index < 0) { + throw new JSONException("JSONArray[" + index + "] not found."); + } + if (index < length()) { + this.myArrayList.set(index, value); + } else { + while (index != length()) { + put(JSONObject.NULL); + } + put(value); + } + return this; + } + + + /** + * Remove an index and close the hole. + * @param index The index of the element to be removed. + * @return The value that was associated with the index, + * or null if there was no value. + */ + public Object remove(int index) { + Object o = opt(index); + this.myArrayList.remove(index); + return o; + } + + + /** + * Produce a JSONObject by combining a JSONArray of names with the values + * of this JSONArray. + * @param names A JSONArray containing a list of key strings. These will be + * paired with the values. + * @return A JSONObject, or null if there are no names or if this JSONArray + * has no values. + * @throws JSONException If any of the names are null. + */ + public JSONObject toJSONObject(JSONArray names) throws JSONException { + if (names == null || names.length() == 0 || length() == 0) { + return null; + } + JSONObject jo = new JSONObject(); + for (int i = 0; i < names.length(); i += 1) { + jo.put(names.getString(i), this.opt(i)); + } + return jo; + } + + + /** + * Make a JSON text of this JSONArray. For compactness, no + * unnecessary whitespace is added. If it is not possible to produce a + * syntactically correct JSON text then null will be returned instead. This + * could occur if the array contains an invalid number. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @return a printable, displayable, transmittable + * representation of the array. + */ + public String toString() { + try { + return '[' + join(",") + ']'; + } catch (Exception e) { + return null; + } + } + + + /** + * Make a prettyprinted JSON text of this JSONArray. + * Warning: This method assumes that the data structure is acyclical. + * @param indentFactor The number of spaces to add to each level of + * indentation. + * @return a printable, displayable, transmittable + * representation of the object, beginning + * with [ (left bracket) and ending + * with ] (right bracket). + * @throws JSONException + */ + public String toString(int indentFactor) throws JSONException { + return toString(indentFactor, 0); + } + + + /** + * Make a prettyprinted JSON text of this JSONArray. + * Warning: This method assumes that the data structure is acyclical. + * @param indentFactor The number of spaces to add to each level of + * indentation. + * @param indent The indention of the top level. + * @return a printable, displayable, transmittable + * representation of the array. + * @throws JSONException + */ + String toString(int indentFactor, int indent) throws JSONException { + int len = length(); + if (len == 0) { + return "[]"; + } + int i; + StringBuffer sb = new StringBuffer("["); + if (len == 1) { + sb.append(JSONObject.valueToString(this.myArrayList.get(0), + indentFactor, indent)); + } else { + int newindent = indent + indentFactor; + sb.append('\n'); + for (i = 0; i < len; i += 1) { + if (i > 0) { + sb.append(",\n"); + } + for (int j = 0; j < newindent; j += 1) { + sb.append(' '); + } + sb.append(JSONObject.valueToString(this.myArrayList.get(i), + indentFactor, newindent)); + } + sb.append('\n'); + for (i = 0; i < indent; i += 1) { + sb.append(' '); + } + } + sb.append(']'); + return sb.toString(); + } + + + /** + * Write the contents of the JSONArray as JSON text to a writer. + * For compactness, no whitespace is added. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @return The writer. + * @throws JSONException + */ + public Writer write(Writer writer) throws JSONException { + try { + boolean b = false; + int len = length(); + + writer.write('['); + + for (int i = 0; i < len; i += 1) { + if (b) { + writer.write(','); + } + Object v = this.myArrayList.get(i); + if (v instanceof JSONObject) { + ((JSONObject)v).write(writer); + } else if (v instanceof JSONArray) { + ((JSONArray)v).write(writer); + } else { + writer.write(JSONObject.valueToString(v)); + } + b = true; + } + writer.write(']'); + return writer; + } catch (IOException e) { + throw new JSONException(e); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/json/JSONException.java b/src/main/java/org/json/JSONException.java new file mode 100644 index 0000000..45e3b8d --- /dev/null +++ b/src/main/java/org/json/JSONException.java @@ -0,0 +1,31 @@ +package org.json; + +/** + * The JSONException is thrown by the JSON.org classes when things are amiss. + * @author JSON.org + * @version 2008-09-18 + */ +public class JSONException extends Exception { + /** + * + */ + private static final long serialVersionUID = 0; + private Throwable cause; + + /** + * Constructs a JSONException with an explanatory message. + * @param message Detail about the reason for the exception. + */ + public JSONException(String message) { + super(message); + } + + public JSONException(Throwable t) { + super(t.getMessage()); + this.cause = t; + } + + public Throwable getCause() { + return this.cause; + } +} diff --git a/src/main/java/org/json/JSONML.java b/src/main/java/org/json/JSONML.java new file mode 100644 index 0000000..1337182 --- /dev/null +++ b/src/main/java/org/json/JSONML.java @@ -0,0 +1,455 @@ +package org.json; + +/* +Copyright (c) 2008 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import java.util.Iterator; + + +/** + * This provides static methods to convert an XML text into a JSONArray or + * JSONObject, and to covert a JSONArray or JSONObject into an XML text using + * the JsonML transform. + * @author JSON.org + * @version 2010-02-12 + */ +public class JSONML { + + /** + * Parse XML values and store them in a JSONArray. + * @param x The XMLTokener containing the source string. + * @param arrayForm true if array form, false if object form. + * @param ja The JSONArray that is containing the current tag or null + * if we are at the outermost level. + * @return A JSONArray if the value is the outermost tag, otherwise null. + * @throws JSONException + */ + private static Object parse(XMLTokener x, boolean arrayForm, + JSONArray ja) throws JSONException { + String attribute; + char c; + String closeTag = null; + int i; + JSONArray newja = null; + JSONObject newjo = null; + Object token; + String tagName = null; + +// Test for and skip past these forms: +// +// +// +// + + while (true) { + token = x.nextContent(); + if (token == XML.LT) { + token = x.nextToken(); + if (token instanceof Character) { + if (token == XML.SLASH) { + +// Close tag "); + } + x.back(); + } else if (c == '[') { + token = x.nextToken(); + if (token.equals("CDATA") && x.next() == '[') { + if (ja != null) { + ja.put(x.nextCDATA()); + } + } else { + throw x.syntaxError("Expected 'CDATA['"); + } + } else { + i = 1; + do { + token = x.nextMeta(); + if (token == null) { + throw x.syntaxError("Missing '>' after ' 0); + } + } else if (token == XML.QUEST) { + +// "); + } else { + throw x.syntaxError("Misshaped tag"); + } + +// Open tag < + + } else { + if (!(token instanceof String)) { + throw x.syntaxError("Bad tagName '" + token + "'."); + } + tagName = (String)token; + newja = new JSONArray(); + newjo = new JSONObject(); + if (arrayForm) { + newja.put(tagName); + if (ja != null) { + ja.put(newja); + } + } else { + newjo.put("tagName", tagName); + if (ja != null) { + ja.put(newjo); + } + } + token = null; + for (;;) { + if (token == null) { + token = x.nextToken(); + } + if (token == null) { + throw x.syntaxError("Misshaped tag"); + } + if (!(token instanceof String)) { + break; + } + +// attribute = value + + attribute = (String)token; + if (!arrayForm && (attribute == "tagName" || attribute == "childNode")) { + throw x.syntaxError("Reserved attribute."); + } + token = x.nextToken(); + if (token == XML.EQ) { + token = x.nextToken(); + if (!(token instanceof String)) { + throw x.syntaxError("Missing value"); + } + newjo.accumulate(attribute, JSONObject.stringToValue((String)token)); + token = null; + } else { + newjo.accumulate(attribute, ""); + } + } + if (arrayForm && newjo.length() > 0) { + newja.put(newjo); + } + +// Empty tag <.../> + + if (token == XML.SLASH) { + if (x.nextToken() != XML.GT) { + throw x.syntaxError("Misshaped tag"); + } + if (ja == null) { + if (arrayForm) { + return newja; + } else { + return newjo; + } + } + +// Content, between <...> and + + } else { + if (token != XML.GT) { + throw x.syntaxError("Misshaped tag"); + } + closeTag = (String)parse(x, arrayForm, newja); + if (closeTag != null) { + if (!closeTag.equals(tagName)) { + throw x.syntaxError("Mismatched '" + tagName + + "' and '" + closeTag + "'"); + } + tagName = null; + if (!arrayForm && newja.length() > 0) { + newjo.put("childNodes", newja); + } + if (ja == null) { + if (arrayForm) { + return newja; + } else { + return newjo; + } + } + } + } + } + } else { + if (ja != null) { + ja.put(token instanceof String ? + JSONObject.stringToValue((String)token) : token); + } + } + } + } + + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONArray using the JsonML transform. Each XML tag is represented as + * a JSONArray in which the first element is the tag name. If the tag has + * attributes, then the second element will be JSONObject containing the + * name/value pairs. If the tag contains children, then strings and + * JSONArrays will represent the child tags. + * Comments, prologs, DTDs, and <[ [ ]]> are ignored. + * @param string The source string. + * @return A JSONArray containing the structured data from the XML string. + * @throws JSONException + */ + public static JSONArray toJSONArray(String string) throws JSONException { + return toJSONArray(new XMLTokener(string)); + } + + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONArray using the JsonML transform. Each XML tag is represented as + * a JSONArray in which the first element is the tag name. If the tag has + * attributes, then the second element will be JSONObject containing the + * name/value pairs. If the tag contains children, then strings and + * JSONArrays will represent the child content and tags. + * Comments, prologs, DTDs, and <[ [ ]]> are ignored. + * @param x An XMLTokener. + * @return A JSONArray containing the structured data from the XML string. + * @throws JSONException + */ + public static JSONArray toJSONArray(XMLTokener x) throws JSONException { + return (JSONArray)parse(x, true, null); + } + + + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONObject using the JsonML transform. Each XML tag is represented as + * a JSONObject with a "tagName" property. If the tag has attributes, then + * the attributes will be in the JSONObject as properties. If the tag + * contains children, the object will have a "childNodes" property which + * will be an array of strings and JsonML JSONObjects. + + * Comments, prologs, DTDs, and <[ [ ]]> are ignored. + * @param x An XMLTokener of the XML source text. + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException + */ + public static JSONObject toJSONObject(XMLTokener x) throws JSONException { + return (JSONObject)parse(x, false, null); + } + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONObject using the JsonML transform. Each XML tag is represented as + * a JSONObject with a "tagName" property. If the tag has attributes, then + * the attributes will be in the JSONObject as properties. If the tag + * contains children, the object will have a "childNodes" property which + * will be an array of strings and JsonML JSONObjects. + + * Comments, prologs, DTDs, and <[ [ ]]> are ignored. + * @param string The XML source text. + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException + */ + public static JSONObject toJSONObject(String string) throws JSONException { + return toJSONObject(new XMLTokener(string)); + } + + + /** + * Reverse the JSONML transformation, making an XML text from a JSONArray. + * @param ja A JSONArray. + * @return An XML string. + * @throws JSONException + */ + public static String toString(JSONArray ja) throws JSONException { + Object e; + int i; + JSONObject jo; + String k; + Iterator keys; + int length; + StringBuffer sb = new StringBuffer(); + String tagName; + String v; + +// Emit = length) { + sb.append('/'); + sb.append('>'); + } else { + sb.append('>'); + do { + e = ja.get(i); + i += 1; + if (e != null) { + if (e instanceof String) { + sb.append(XML.escape(e.toString())); + } else if (e instanceof JSONObject) { + sb.append(toString((JSONObject)e)); + } else if (e instanceof JSONArray) { + sb.append(toString((JSONArray)e)); + } + } + } while (i < length); + sb.append('<'); + sb.append('/'); + sb.append(tagName); + sb.append('>'); + } + return sb.toString(); + } + + /** + * Reverse the JSONML transformation, making an XML text from a JSONObject. + * The JSONObject must contain a "tagName" property. If it has children, + * then it must have a "childNodes" property containing an array of objects. + * The other properties are attributes with string values. + * @param jo A JSONObject. + * @return An XML string. + * @throws JSONException + */ + public static String toString(JSONObject jo) throws JSONException { + StringBuffer sb = new StringBuffer(); + Object e; + int i; + JSONArray ja; + String k; + Iterator keys; + int len; + String tagName; + String v; + +//Emit '); + } else { + sb.append('>'); + len = ja.length(); + for (i = 0; i < len; i += 1) { + e = ja.get(i); + if (e != null) { + if (e instanceof String) { + sb.append(XML.escape(e.toString())); + } else if (e instanceof JSONObject) { + sb.append(toString((JSONObject)e)); + } else if (e instanceof JSONArray) { + sb.append(toString((JSONArray)e)); + } + } + } + sb.append('<'); + sb.append('/'); + sb.append(tagName); + sb.append('>'); + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/org/json/JSONObject.java b/src/main/java/org/json/JSONObject.java new file mode 100644 index 0000000..419ff94 --- /dev/null +++ b/src/main/java/org/json/JSONObject.java @@ -0,0 +1,1583 @@ +package org.json; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import java.io.IOException; +import java.io.Writer; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.TreeSet; + +/** + * A JSONObject is an unordered collection of name/value pairs. Its + * external form is a string wrapped in curly braces with colons between the + * names and values, and commas between the values and names. The internal form + * is an object having get and opt methods for + * accessing the values by name, and put methods for adding or + * replacing values by name. The values can be any of these types: + * Boolean, JSONArray, JSONObject, + * Number, String, or the JSONObject.NULL + * object. A JSONObject constructor can be used to convert an external form + * JSON text into an internal form whose values can be retrieved with the + * get and opt methods, or to convert values into a + * JSON text using the put and toString methods. + * A get method returns a value if one can be found, and throws an + * exception if one cannot be found. An opt method returns a + * default value instead of throwing an exception, and so is useful for + * obtaining optional values. + *

+ * The generic get() and opt() methods return an + * object, which you can cast or query for type. There are also typed + * get and opt methods that do type checking and type + * coercion for you. + *

+ * The put methods adds values to an object. For example,

+ *     myString = new JSONObject().put("JSON", "Hello, World!").toString();
+ * produces the string {"JSON": "Hello, World"}. + *

+ * The texts produced by the toString methods strictly conform to + * the JSON syntax rules. + * The constructors are more forgiving in the texts they will accept: + *

    + *
  • An extra , (comma) may appear just + * before the closing brace.
  • + *
  • Strings may be quoted with ' (single + * quote).
  • + *
  • Strings do not need to be quoted at all if they do not begin with a quote + * or single quote, and if they do not contain leading or trailing spaces, + * and if they do not contain any of these characters: + * { } [ ] / \ : , = ; # and if they do not look like numbers + * and if they are not the reserved words true, + * false, or null.
  • + *
  • Keys can be followed by = or => as well as + * by :.
  • + *
  • Values can be followed by ; (semicolon) as + * well as by , (comma).
  • + *
  • Numbers may have the 0x- (hex) prefix.
  • + *
+ * @author JSON.org + * @version 2010-04-20 + */ +public class JSONObject { + + /** + * JSONObject.NULL is equivalent to the value that JavaScript calls null, + * whilst Java's null is equivalent to the value that JavaScript calls + * undefined. + */ + private static final class Null { + + /** + * There is only intended to be a single instance of the NULL object, + * so the clone method returns itself. + * @return NULL. + */ + protected final Object clone() { + return this; + } + + + /** + * A Null object is equal to the null value and to itself. + * @param object An object to test for nullness. + * @return true if the object parameter is the JSONObject.NULL object + * or null. + */ + public boolean equals(Object object) { + return object == null || object == this; + } + + + /** + * Get the "null" string value. + * @return The string "null". + */ + public String toString() { + return "null"; + } + } + + + /** + * The map where the JSONObject's properties are kept. + */ + private Map map; + + + /** + * It is sometimes more convenient and less ambiguous to have a + * NULL object than to use Java's null value. + * JSONObject.NULL.equals(null) returns true. + * JSONObject.NULL.toString() returns "null". + */ + public static final Object NULL = new Null(); + + + /** + * Construct an empty JSONObject. + */ + public JSONObject() { + this.map = new HashMap(); + } + + + /** + * Construct a JSONObject from a subset of another JSONObject. + * An array of strings is used to identify the keys that should be copied. + * Missing keys are ignored. + * @param jo A JSONObject. + * @param names An array of strings. + * @throws JSONException + * @exception JSONException If a value is a non-finite number or if a name is duplicated. + */ + public JSONObject(JSONObject jo, String[] names) { + this(); + for (int i = 0; i < names.length; i += 1) { + try { + putOnce(names[i], jo.opt(names[i])); + } catch (Exception ignore) { + } + } + } + + + /** + * Construct a JSONObject from a JSONTokener. + * @param x A JSONTokener object containing the source string. + * @throws JSONException If there is a syntax error in the source string + * or a duplicated key. + */ + public JSONObject(JSONTokener x) throws JSONException { + this(); + char c; + String key; + + if (x.nextClean() != '{') { + throw x.syntaxError("A JSONObject text must begin with '{'"); + } + for (;;) { + c = x.nextClean(); + switch (c) { + case 0: + throw x.syntaxError("A JSONObject text must end with '}'"); + case '}': + return; + default: + x.back(); + key = x.nextValue().toString(); + } + + /* + * The key is followed by ':'. We will also tolerate '=' or '=>'. + */ + + c = x.nextClean(); + if (c == '=') { + if (x.next() != '>') { + x.back(); + } + } else if (c != ':') { + throw x.syntaxError("Expected a ':' after a key"); + } + putOnce(key, x.nextValue()); + + /* + * Pairs are separated by ','. We will also tolerate ';'. + */ + + switch (x.nextClean()) { + case ';': + case ',': + if (x.nextClean() == '}') { + return; + } + x.back(); + break; + case '}': + return; + default: + throw x.syntaxError("Expected a ',' or '}'"); + } + } + } + + + /** + * Construct a JSONObject from a Map. + * + * @param map A map object that can be used to initialize the contents of + * the JSONObject. + * @throws JSONException + */ + public JSONObject(Map map) { + this.map = new HashMap(); + if (map != null) { + Iterator i = map.entrySet().iterator(); + while (i.hasNext()) { + Map.Entry e = (Map.Entry)i.next(); + this.map.put(e.getKey(), wrap(e.getValue())); + } + } + } + + + /** + * Construct a JSONObject from an Object using bean getters. + * It reflects on all of the public methods of the object. + * For each of the methods with no parameters and a name starting + * with "get" or "is" followed by an uppercase letter, + * the method is invoked, and a key and the value returned from the getter method + * are put into the new JSONObject. + * + * The key is formed by removing the "get" or "is" prefix. + * If the second remaining character is not upper case, then the first + * character is converted to lower case. + * + * For example, if an object has a method named "getName", and + * if the result of calling object.getName() is "Larry Fine", + * then the JSONObject will contain "name": "Larry Fine". + * + * @param bean An object that has getter methods that should be used + * to make a JSONObject. + */ + public JSONObject(Object bean) { + this(); + populateMap(bean); + } + + + /** + * Construct a JSONObject from an Object, using reflection to find the + * public members. The resulting JSONObject's keys will be the strings + * from the names array, and the values will be the field values associated + * with those keys in the object. If a key is not found or not visible, + * then it will not be copied into the new JSONObject. + * @param object An object that has fields that should be used to make a + * JSONObject. + * @param names An array of strings, the names of the fields to be obtained + * from the object. + */ + public JSONObject(Object object, String names[]) { + this(); + Class c = object.getClass(); + for (int i = 0; i < names.length; i += 1) { + String name = names[i]; + try { + putOpt(name, c.getField(name).get(object)); + } catch (Exception ignore) { + } + } + } + + + /** + * Construct a JSONObject from a source JSON text string. + * This is the most commonly used JSONObject constructor. + * @param source A string beginning + * with { (left brace) and ending + * with } (right brace). + * @exception JSONException If there is a syntax error in the source + * string or a duplicated key. + */ + public JSONObject(String source) throws JSONException { + this(new JSONTokener(source)); + } + + + /** + * Accumulate values under a key. It is similar to the put method except + * that if there is already an object stored under the key then a + * JSONArray is stored under the key to hold all of the accumulated values. + * If there is already a JSONArray, then the new value is appended to it. + * In contrast, the put method replaces the previous value. + * @param key A key string. + * @param value An object to be accumulated under the key. + * @return this. + * @throws JSONException If the value is an invalid number + * or if the key is null. + */ + public JSONObject accumulate(String key, Object value) + throws JSONException { + testValidity(value); + Object o = opt(key); + if (o == null) { + put(key, value instanceof JSONArray ? + new JSONArray().put(value) : + value); + } else if (o instanceof JSONArray) { + ((JSONArray)o).put(value); + } else { + put(key, new JSONArray().put(o).put(value)); + } + return this; + } + + + /** + * Append values to the array under a key. If the key does not exist in the + * JSONObject, then the key is put in the JSONObject with its value being a + * JSONArray containing the value parameter. If the key was already + * associated with a JSONArray, then the value parameter is appended to it. + * @param key A key string. + * @param value An object to be accumulated under the key. + * @return this. + * @throws JSONException If the key is null or if the current value + * associated with the key is not a JSONArray. + */ + public JSONObject append(String key, Object value) throws JSONException { + testValidity(value); + Object o = opt(key); + if (o == null) { + put(key, new JSONArray().put(value)); + } else if (o instanceof JSONArray) { + put(key, ((JSONArray)o).put(value)); + } else { + throw new JSONException("JSONObject[" + key + + "] is not a JSONArray."); + } + return this; + } + + + /** + * Produce a string from a double. The string "null" will be returned if + * the number is not finite. + * @param d A double. + * @return A String. + */ + static public String doubleToString(double d) { + if (Double.isInfinite(d) || Double.isNaN(d)) { + return "null"; + } + +// Shave off trailing zeros and decimal point, if possible. + + String s = Double.toString(d); + if (s.indexOf('.') > 0 && s.indexOf('e') < 0 && s.indexOf('E') < 0) { + while (s.endsWith("0")) { + s = s.substring(0, s.length() - 1); + } + if (s.endsWith(".")) { + s = s.substring(0, s.length() - 1); + } + } + return s; + } + + + /** + * Get the value object associated with a key. + * + * @param key A key string. + * @return The object associated with the key. + * @throws JSONException if the key is not found. + */ + public Object get(String key) throws JSONException { + Object o = opt(key); + if (o == null) { + throw new JSONException("JSONObject[" + quote(key) + + "] not found."); + } + return o; + } + + + /** + * Get the boolean value associated with a key. + * + * @param key A key string. + * @return The truth. + * @throws JSONException + * if the value is not a Boolean or the String "true" or "false". + */ + public boolean getBoolean(String key) throws JSONException { + Object o = get(key); + if (o.equals(Boolean.FALSE) || + (o instanceof String && + ((String)o).equalsIgnoreCase("false"))) { + return false; + } else if (o.equals(Boolean.TRUE) || + (o instanceof String && + ((String)o).equalsIgnoreCase("true"))) { + return true; + } + throw new JSONException("JSONObject[" + quote(key) + + "] is not a Boolean."); + } + + + /** + * Get the double value associated with a key. + * @param key A key string. + * @return The numeric value. + * @throws JSONException if the key is not found or + * if the value is not a Number object and cannot be converted to a number. + */ + public double getDouble(String key) throws JSONException { + Object o = get(key); + try { + return o instanceof Number ? + ((Number)o).doubleValue() : + Double.valueOf((String)o).doubleValue(); + } catch (Exception e) { + throw new JSONException("JSONObject[" + quote(key) + + "] is not a number."); + } + } + + + /** + * Get the int value associated with a key. + * + * @param key A key string. + * @return The integer value. + * @throws JSONException if the key is not found or if the value cannot + * be converted to an integer. + */ + public int getInt(String key) throws JSONException { + Object o = get(key); + try { + return o instanceof Number ? + ((Number)o).intValue() : + Integer.parseInt((String)o); + } catch (Exception e) { + throw new JSONException("JSONObject[" + quote(key) + + "] is not an int."); + } + } + + + /** + * Get the JSONArray value associated with a key. + * + * @param key A key string. + * @return A JSONArray which is the value. + * @throws JSONException if the key is not found or + * if the value is not a JSONArray. + */ + public JSONArray getJSONArray(String key) throws JSONException { + Object o = get(key); + if (o instanceof JSONArray) { + return (JSONArray)o; + } + throw new JSONException("JSONObject[" + quote(key) + + "] is not a JSONArray."); + } + + + /** + * Get the JSONObject value associated with a key. + * + * @param key A key string. + * @return A JSONObject which is the value. + * @throws JSONException if the key is not found or + * if the value is not a JSONObject. + */ + public JSONObject getJSONObject(String key) throws JSONException { + Object o = get(key); + if (o instanceof JSONObject) { + return (JSONObject)o; + } + throw new JSONException("JSONObject[" + quote(key) + + "] is not a JSONObject."); + } + + + /** + * Get the long value associated with a key. + * + * @param key A key string. + * @return The long value. + * @throws JSONException if the key is not found or if the value cannot + * be converted to a long. + */ + public long getLong(String key) throws JSONException { + Object o = get(key); + try { + return o instanceof Number ? + ((Number)o).longValue() : + Long.parseLong((String)o); + } catch (Exception e) { + throw new JSONException("JSONObject[" + quote(key) + + "] is not a long."); + } + } + + + /** + * Get an array of field names from a JSONObject. + * + * @return An array of field names, or null if there are no names. + */ + public static String[] getNames(JSONObject jo) { + int length = jo.length(); + if (length == 0) { + return null; + } + Iterator i = jo.keys(); + String[] names = new String[length]; + int j = 0; + while (i.hasNext()) { + names[j] = (String)i.next(); + j += 1; + } + return names; + } + + + /** + * Get an array of field names from an Object. + * + * @return An array of field names, or null if there are no names. + */ + public static String[] getNames(Object object) { + if (object == null) { + return null; + } + Class klass = object.getClass(); + Field[] fields = klass.getFields(); + int length = fields.length; + if (length == 0) { + return null; + } + String[] names = new String[length]; + for (int i = 0; i < length; i += 1) { + names[i] = fields[i].getName(); + } + return names; + } + + + /** + * Get the string associated with a key. + * + * @param key A key string. + * @return A string which is the value. + * @throws JSONException if the key is not found. + */ + public String getString(String key) throws JSONException { + return get(key).toString(); + } + + + /** + * Determine if the JSONObject contains a specific key. + * @param key A key string. + * @return true if the key exists in the JSONObject. + */ + public boolean has(String key) { + return this.map.containsKey(key); + } + + + /** + * Increment a property of a JSONObject. If there is no such property, + * create one with a value of 1. If there is such a property, and if + * it is an Integer, Long, Double, or Float, then add one to it. + * @param key A key string. + * @return this. + * @throws JSONException If there is already a property with this name + * that is not an Integer, Long, Double, or Float. + */ + public JSONObject increment(String key) throws JSONException { + Object value = opt(key); + if (value == null) { + put(key, 1); + } else { + if (value instanceof Integer) { + put(key, ((Integer)value).intValue() + 1); + } else if (value instanceof Long) { + put(key, ((Long)value).longValue() + 1); + } else if (value instanceof Double) { + put(key, ((Double)value).doubleValue() + 1); + } else if (value instanceof Float) { + put(key, ((Float)value).floatValue() + 1); + } else { + throw new JSONException("Unable to increment [" + key + "]."); + } + } + return this; + } + + + /** + * Determine if the value associated with the key is null or if there is + * no value. + * @param key A key string. + * @return true if there is no value associated with the key or if + * the value is the JSONObject.NULL object. + */ + public boolean isNull(String key) { + return JSONObject.NULL.equals(opt(key)); + } + + + /** + * Get an enumeration of the keys of the JSONObject. + * + * @return An iterator of the keys. + */ + public Iterator keys() { + return this.map.keySet().iterator(); + } + + + /** + * Get the number of keys stored in the JSONObject. + * + * @return The number of keys in the JSONObject. + */ + public int length() { + return this.map.size(); + } + + + /** + * Produce a JSONArray containing the names of the elements of this + * JSONObject. + * @return A JSONArray containing the key strings, or null if the JSONObject + * is empty. + */ + public JSONArray names() { + JSONArray ja = new JSONArray(); + Iterator keys = keys(); + while (keys.hasNext()) { + ja.put(keys.next()); + } + return ja.length() == 0 ? null : ja; + } + + /** + * Produce a string from a Number. + * @param n A Number + * @return A String. + * @throws JSONException If n is a non-finite number. + */ + static public String numberToString(Number n) + throws JSONException { + if (n == null) { + throw new JSONException("Null pointer"); + } + testValidity(n); + +// Shave off trailing zeros and decimal point, if possible. + + String s = n.toString(); + if (s.indexOf('.') > 0 && s.indexOf('e') < 0 && s.indexOf('E') < 0) { + while (s.endsWith("0")) { + s = s.substring(0, s.length() - 1); + } + if (s.endsWith(".")) { + s = s.substring(0, s.length() - 1); + } + } + return s; + } + + + /** + * Get an optional value associated with a key. + * @param key A key string. + * @return An object which is the value, or null if there is no value. + */ + public Object opt(String key) { + return key == null ? null : this.map.get(key); + } + + + /** + * Get an optional boolean associated with a key. + * It returns false if there is no such key, or if the value is not + * Boolean.TRUE or the String "true". + * + * @param key A key string. + * @return The truth. + */ + public boolean optBoolean(String key) { + return optBoolean(key, false); + } + + + /** + * Get an optional boolean associated with a key. + * It returns the defaultValue if there is no such key, or if it is not + * a Boolean or the String "true" or "false" (case insensitive). + * + * @param key A key string. + * @param defaultValue The default. + * @return The truth. + */ + public boolean optBoolean(String key, boolean defaultValue) { + try { + return getBoolean(key); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get an optional double associated with a key, + * or NaN if there is no such key or if its value is not a number. + * If the value is a string, an attempt will be made to evaluate it as + * a number. + * + * @param key A string which is the key. + * @return An object which is the value. + */ + public double optDouble(String key) { + return optDouble(key, Double.NaN); + } + + + /** + * Get an optional double associated with a key, or the + * defaultValue if there is no such key or if its value is not a number. + * If the value is a string, an attempt will be made to evaluate it as + * a number. + * + * @param key A key string. + * @param defaultValue The default. + * @return An object which is the value. + */ + public double optDouble(String key, double defaultValue) { + try { + Object o = opt(key); + return o instanceof Number ? ((Number)o).doubleValue() : + new Double((String)o).doubleValue(); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get an optional int value associated with a key, + * or zero if there is no such key or if the value is not a number. + * If the value is a string, an attempt will be made to evaluate it as + * a number. + * + * @param key A key string. + * @return An object which is the value. + */ + public int optInt(String key) { + return optInt(key, 0); + } + + + /** + * Get an optional int value associated with a key, + * or the default if there is no such key or if the value is not a number. + * If the value is a string, an attempt will be made to evaluate it as + * a number. + * + * @param key A key string. + * @param defaultValue The default. + * @return An object which is the value. + */ + public int optInt(String key, int defaultValue) { + try { + return getInt(key); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get an optional JSONArray associated with a key. + * It returns null if there is no such key, or if its value is not a + * JSONArray. + * + * @param key A key string. + * @return A JSONArray which is the value. + */ + public JSONArray optJSONArray(String key) { + Object o = opt(key); + return o instanceof JSONArray ? (JSONArray)o : null; + } + + + /** + * Get an optional JSONObject associated with a key. + * It returns null if there is no such key, or if its value is not a + * JSONObject. + * + * @param key A key string. + * @return A JSONObject which is the value. + */ + public JSONObject optJSONObject(String key) { + Object o = opt(key); + return o instanceof JSONObject ? (JSONObject)o : null; + } + + + /** + * Get an optional long value associated with a key, + * or zero if there is no such key or if the value is not a number. + * If the value is a string, an attempt will be made to evaluate it as + * a number. + * + * @param key A key string. + * @return An object which is the value. + */ + public long optLong(String key) { + return optLong(key, 0); + } + + + /** + * Get an optional long value associated with a key, + * or the default if there is no such key or if the value is not a number. + * If the value is a string, an attempt will be made to evaluate it as + * a number. + * + * @param key A key string. + * @param defaultValue The default. + * @return An object which is the value. + */ + public long optLong(String key, long defaultValue) { + try { + return getLong(key); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get an optional string associated with a key. + * It returns an empty string if there is no such key. If the value is not + * a string and is not null, then it is coverted to a string. + * + * @param key A key string. + * @return A string which is the value. + */ + public String optString(String key) { + return optString(key, ""); + } + + + /** + * Get an optional string associated with a key. + * It returns the defaultValue if there is no such key. + * + * @param key A key string. + * @param defaultValue The default. + * @return A string which is the value. + */ + public String optString(String key, String defaultValue) { + Object o = opt(key); + return o != null ? o.toString() : defaultValue; + } + + + private void populateMap(Object bean) { + Class klass = bean.getClass(); + +// If klass is a System class then set includeSuperClass to false. + + boolean includeSuperClass = klass.getClassLoader() != null; + + Method[] methods = (includeSuperClass) ? + klass.getMethods() : klass.getDeclaredMethods(); + for (int i = 0; i < methods.length; i += 1) { + try { + Method method = methods[i]; + if (Modifier.isPublic(method.getModifiers())) { + String name = method.getName(); + String key = ""; + if (name.startsWith("get")) { + if (name.equals("getClass") || + name.equals("getDeclaringClass")) { + key = ""; + } else { + key = name.substring(3); + } + } else if (name.startsWith("is")) { + key = name.substring(2); + } + if (key.length() > 0 && + Character.isUpperCase(key.charAt(0)) && + method.getParameterTypes().length == 0) { + if (key.length() == 1) { + key = key.toLowerCase(); + } else if (!Character.isUpperCase(key.charAt(1))) { + key = key.substring(0, 1).toLowerCase() + + key.substring(1); + } + + Object result = method.invoke(bean, (Object[])null); + + map.put(key, wrap(result)); + } + } + } catch (Exception ignore) { + } + } + } + + + /** + * Put a key/boolean pair in the JSONObject. + * + * @param key A key string. + * @param value A boolean which is the value. + * @return this. + * @throws JSONException If the key is null. + */ + public JSONObject put(String key, boolean value) throws JSONException { + put(key, value ? Boolean.TRUE : Boolean.FALSE); + return this; + } + + + /** + * Put a key/value pair in the JSONObject, where the value will be a + * JSONArray which is produced from a Collection. + * @param key A key string. + * @param value A Collection value. + * @return this. + * @throws JSONException + */ + public JSONObject put(String key, Collection value) throws JSONException { + put(key, new JSONArray(value)); + return this; + } + + + /** + * Put a key/double pair in the JSONObject. + * + * @param key A key string. + * @param value A double which is the value. + * @return this. + * @throws JSONException If the key is null or if the number is invalid. + */ + public JSONObject put(String key, double value) throws JSONException { + put(key, new Double(value)); + return this; + } + + + /** + * Put a key/int pair in the JSONObject. + * + * @param key A key string. + * @param value An int which is the value. + * @return this. + * @throws JSONException If the key is null. + */ + public JSONObject put(String key, int value) throws JSONException { + put(key, new Integer(value)); + return this; + } + + + /** + * Put a key/long pair in the JSONObject. + * + * @param key A key string. + * @param value A long which is the value. + * @return this. + * @throws JSONException If the key is null. + */ + public JSONObject put(String key, long value) throws JSONException { + put(key, new Long(value)); + return this; + } + + + /** + * Put a key/value pair in the JSONObject, where the value will be a + * JSONObject which is produced from a Map. + * @param key A key string. + * @param value A Map value. + * @return this. + * @throws JSONException + */ + public JSONObject put(String key, Map value) throws JSONException { + put(key, new JSONObject(value)); + return this; + } + + + /** + * Put a key/value pair in the JSONObject. If the value is null, + * then the key will be removed from the JSONObject if it is present. + * @param key A key string. + * @param value An object which is the value. It should be of one of these + * types: Boolean, Double, Integer, JSONArray, JSONObject, Long, String, + * or the JSONObject.NULL object. + * @return this. + * @throws JSONException If the value is non-finite number + * or if the key is null. + */ + public JSONObject put(String key, Object value) throws JSONException { + if (key == null) { + throw new JSONException("Null key."); + } + if (value != null) { + testValidity(value); + this.map.put(key, value); + } else { + remove(key); + } + return this; + } + + + /** + * Put a key/value pair in the JSONObject, but only if the key and the + * value are both non-null, and only if there is not already a member + * with that name. + * @param key + * @param value + * @return his. + * @throws JSONException if the key is a duplicate + */ + public JSONObject putOnce(String key, Object value) throws JSONException { + if (key != null && value != null) { + if (opt(key) != null) { + throw new JSONException("Duplicate key \"" + key + "\""); + } + put(key, value); + } + return this; + } + + + /** + * Put a key/value pair in the JSONObject, but only if the + * key and the value are both non-null. + * @param key A key string. + * @param value An object which is the value. It should be of one of these + * types: Boolean, Double, Integer, JSONArray, JSONObject, Long, String, + * or the JSONObject.NULL object. + * @return this. + * @throws JSONException If the value is a non-finite number. + */ + public JSONObject putOpt(String key, Object value) throws JSONException { + if (key != null && value != null) { + put(key, value); + } + return this; + } + + + /** + * Produce a string in double quotes with backslash sequences in all the + * right places. A backslash will be inserted within = '\u0080' && c < '\u00a0') || + (c >= '\u2000' && c < '\u2100')) { + t = "000" + Integer.toHexString(c); + sb.append("\\u" + t.substring(t.length() - 4)); + } else { + sb.append(c); + } + } + } + sb.append('"'); + return sb.toString(); + } + + /** + * Remove a name and its value, if present. + * @param key The name to be removed. + * @return The value that was associated with the name, + * or null if there was no value. + */ + public Object remove(String key) { + return this.map.remove(key); + } + + /** + * Get an enumeration of the keys of the JSONObject. + * The keys will be sorted alphabetically. + * + * @return An iterator of the keys. + */ + public Iterator sortedKeys() { + return new TreeSet(this.map.keySet()).iterator(); + } + + /** + * Try to convert a string into a number, boolean, or null. If the string + * can't be converted, return the string. + * @param s A String. + * @return A simple JSON value. + */ + static public Object stringToValue(String s) { + if (s.equals("")) { + return s; + } + if (s.equalsIgnoreCase("true")) { + return Boolean.TRUE; + } + if (s.equalsIgnoreCase("false")) { + return Boolean.FALSE; + } + if (s.equalsIgnoreCase("null")) { + return JSONObject.NULL; + } + + /* + * If it might be a number, try converting it. + * We support the non-standard 0x- convention. + * If a number cannot be produced, then the value will just + * be a string. Note that the 0x-, plus, and implied string + * conventions are non-standard. A JSON parser may accept + * non-JSON forms as long as it accepts all correct JSON forms. + */ + + char b = s.charAt(0); + if ((b >= '0' && b <= '9') || b == '.' || b == '-' || b == '+') { + if (b == '0' && s.length() > 2 && + (s.charAt(1) == 'x' || s.charAt(1) == 'X')) { + try { + return new Integer(Integer.parseInt(s.substring(2), 16)); + } catch (Exception ignore) { + } + } + try { + if (s.indexOf('.') > -1 || + s.indexOf('e') > -1 || s.indexOf('E') > -1) { + return Double.valueOf(s); + } else { + Long myLong = new Long(s); + if (myLong.longValue() == myLong.intValue()) { + return new Integer(myLong.intValue()); + } else { + return myLong; + } + } + } catch (Exception ignore) { + } + } + return s; + } + + + /** + * Throw an exception if the object is an NaN or infinite number. + * @param o The object to test. + * @throws JSONException If o is a non-finite number. + */ + static void testValidity(Object o) throws JSONException { + if (o != null) { + if (o instanceof Double) { + if (((Double)o).isInfinite() || ((Double)o).isNaN()) { + throw new JSONException( + "JSON does not allow non-finite numbers."); + } + } else if (o instanceof Float) { + if (((Float)o).isInfinite() || ((Float)o).isNaN()) { + throw new JSONException( + "JSON does not allow non-finite numbers."); + } + } + } + } + + + /** + * Produce a JSONArray containing the values of the members of this + * JSONObject. + * @param names A JSONArray containing a list of key strings. This + * determines the sequence of the values in the result. + * @return A JSONArray of values. + * @throws JSONException If any of the values are non-finite numbers. + */ + public JSONArray toJSONArray(JSONArray names) throws JSONException { + if (names == null || names.length() == 0) { + return null; + } + JSONArray ja = new JSONArray(); + for (int i = 0; i < names.length(); i += 1) { + ja.put(this.opt(names.getString(i))); + } + return ja; + } + + /** + * Make a JSON text of this JSONObject. For compactness, no whitespace + * is added. If this would not result in a syntactically correct JSON text, + * then null will be returned instead. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @return a printable, displayable, portable, transmittable + * representation of the object, beginning + * with { (left brace) and ending + * with } (right brace). + */ + public String toString() { + try { + Iterator keys = keys(); + StringBuffer sb = new StringBuffer("{"); + + while (keys.hasNext()) { + if (sb.length() > 1) { + sb.append(','); + } + Object o = keys.next(); + sb.append(quote(o.toString())); + sb.append(':'); + sb.append(valueToString(this.map.get(o))); + } + sb.append('}'); + return sb.toString(); + } catch (Exception e) { + return null; + } + } + + + /** + * Make a prettyprinted JSON text of this JSONObject. + *

+ * Warning: This method assumes that the data structure is acyclical. + * @param indentFactor The number of spaces to add to each level of + * indentation. + * @return a printable, displayable, portable, transmittable + * representation of the object, beginning + * with { (left brace) and ending + * with } (right brace). + * @throws JSONException If the object contains an invalid number. + */ + public String toString(int indentFactor) throws JSONException { + return toString(indentFactor, 0); + } + + + /** + * Make a prettyprinted JSON text of this JSONObject. + *

+ * Warning: This method assumes that the data structure is acyclical. + * @param indentFactor The number of spaces to add to each level of + * indentation. + * @param indent The indentation of the top level. + * @return a printable, displayable, transmittable + * representation of the object, beginning + * with { (left brace) and ending + * with } (right brace). + * @throws JSONException If the object contains an invalid number. + */ + String toString(int indentFactor, int indent) throws JSONException { + int j; + int n = length(); + if (n == 0) { + return "{}"; + } + Iterator keys = sortedKeys(); + StringBuffer sb = new StringBuffer("{"); + int newindent = indent + indentFactor; + Object o; + if (n == 1) { + o = keys.next(); + sb.append(quote(o.toString())); + sb.append(": "); + sb.append(valueToString(this.map.get(o), indentFactor, + indent)); + } else { + while (keys.hasNext()) { + o = keys.next(); + if (sb.length() > 1) { + sb.append(",\n"); + } else { + sb.append('\n'); + } + for (j = 0; j < newindent; j += 1) { + sb.append(' '); + } + sb.append(quote(o.toString())); + sb.append(": "); + sb.append(valueToString(this.map.get(o), indentFactor, + newindent)); + } + if (sb.length() > 1) { + sb.append('\n'); + for (j = 0; j < indent; j += 1) { + sb.append(' '); + } + } + } + sb.append('}'); + return sb.toString(); + } + + + /** + * Make a JSON text of an Object value. If the object has an + * value.toJSONString() method, then that method will be used to produce + * the JSON text. The method is required to produce a strictly + * conforming text. If the object does not contain a toJSONString + * method (which is the most common case), then a text will be + * produced by other means. If the value is an array or Collection, + * then a JSONArray will be made from it and its toJSONString method + * will be called. If the value is a MAP, then a JSONObject will be made + * from it and its toJSONString method will be called. Otherwise, the + * value's toString method will be called, and the result will be quoted. + * + *

+ * Warning: This method assumes that the data structure is acyclical. + * @param value The value to be serialized. + * @return a printable, displayable, transmittable + * representation of the object, beginning + * with { (left brace) and ending + * with } (right brace). + * @throws JSONException If the value is or contains an invalid number. + */ + static String valueToString(Object value) throws JSONException { + if (value == null || value.equals(null)) { + return "null"; + } + if (value instanceof JSONString) { + Object o; + try { + o = ((JSONString)value).toJSONString(); + } catch (Exception e) { + throw new JSONException(e); + } + if (o instanceof String) { + return (String)o; + } + throw new JSONException("Bad value from toJSONString: " + o); + } + if (value instanceof Number) { + return numberToString((Number) value); + } + if (value instanceof Boolean || value instanceof JSONObject || + value instanceof JSONArray) { + return value.toString(); + } + if (value instanceof Map) { + return new JSONObject((Map)value).toString(); + } + if (value instanceof Collection) { + return new JSONArray((Collection)value).toString(); + } + if (value.getClass().isArray()) { + return new JSONArray(value).toString(); + } + return quote(value.toString()); + } + + + /** + * Make a prettyprinted JSON text of an object value. + *

+ * Warning: This method assumes that the data structure is acyclical. + * @param value The value to be serialized. + * @param indentFactor The number of spaces to add to each level of + * indentation. + * @param indent The indentation of the top level. + * @return a printable, displayable, transmittable + * representation of the object, beginning + * with { (left brace) and ending + * with } (right brace). + * @throws JSONException If the object contains an invalid number. + */ + static String valueToString(Object value, int indentFactor, int indent) + throws JSONException { + if (value == null || value.equals(null)) { + return "null"; + } + try { + if (value instanceof JSONString) { + Object o = ((JSONString)value).toJSONString(); + if (o instanceof String) { + return (String)o; + } + } + } catch (Exception ignore) { + } + if (value instanceof Number) { + return numberToString((Number) value); + } + if (value instanceof Boolean) { + return value.toString(); + } + if (value instanceof JSONObject) { + return ((JSONObject)value).toString(indentFactor, indent); + } + if (value instanceof JSONArray) { + return ((JSONArray)value).toString(indentFactor, indent); + } + if (value instanceof Map) { + return new JSONObject((Map)value).toString(indentFactor, indent); + } + if (value instanceof Collection) { + return new JSONArray((Collection)value).toString(indentFactor, indent); + } + if (value.getClass().isArray()) { + return new JSONArray(value).toString(indentFactor, indent); + } + return quote(value.toString()); + } + + + /** + * Wrap an object, if necessary. If the object is null, return the NULL + * object. If it is an array or collection, wrap it in a JSONArray. If + * it is a map, wrap it in a JSONObject. If it is a standard property + * (Double, String, et al) then it is already wrapped. Otherwise, if it + * comes from one of the java packages, turn it into a string. And if + * it doesn't, try to wrap it in a JSONObject. If the wrapping fails, + * then null is returned. + * + * @param object The object to wrap + * @return The wrapped value + */ + static Object wrap(Object object) { + try { + if (object == null) { + return NULL; + } + if (object instanceof JSONObject || object instanceof JSONArray || + object instanceof Byte || object instanceof Character || + object instanceof Short || object instanceof Integer || + object instanceof Long || object instanceof Boolean || + object instanceof Float || object instanceof Double || + object instanceof String || NULL.equals(object)) { + return object; + } + + if (object instanceof Collection) { + return new JSONArray((Collection)object); + } + if (object.getClass().isArray()) { + return new JSONArray(object); + } + if (object instanceof Map) { + return new JSONObject((Map)object); + } + Package objectPackage = object.getClass().getPackage(); + String objectPackageName = ( objectPackage != null ? objectPackage.getName() : "" ); + if (objectPackageName.startsWith("java.") || + objectPackageName.startsWith("javax.") || + object.getClass().getClassLoader() == null) { + return object.toString(); + } + return new JSONObject(object); + } catch(Exception exception) { + return null; + } + } + + + /** + * Write the contents of the JSONObject as JSON text to a writer. + * For compactness, no whitespace is added. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @return The writer. + * @throws JSONException + */ + public Writer write(Writer writer) throws JSONException { + try { + boolean b = false; + Iterator keys = keys(); + writer.write('{'); + + while (keys.hasNext()) { + if (b) { + writer.write(','); + } + Object k = keys.next(); + writer.write(quote(k.toString())); + writer.write(':'); + Object v = this.map.get(k); + if (v instanceof JSONObject) { + ((JSONObject)v).write(writer); + } else if (v instanceof JSONArray) { + ((JSONArray)v).write(writer); + } else { + writer.write(valueToString(v)); + } + b = true; + } + writer.write('}'); + return writer; + } catch (IOException exception) { + throw new JSONException(exception); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/json/JSONString.java b/src/main/java/org/json/JSONString.java new file mode 100644 index 0000000..17f4384 --- /dev/null +++ b/src/main/java/org/json/JSONString.java @@ -0,0 +1,18 @@ +package org.json; +/** + * The JSONString interface allows a toJSONString() + * method so that a class can change the behavior of + * JSONObject.toString(), JSONArray.toString(), + * and JSONWriter.value(Object). The + * toJSONString method will be used instead of the default behavior + * of using the Object's toString() method and quoting the result. + */ +public interface JSONString { + /** + * The toJSONString method allows a class to produce its own JSON + * serialization. + * + * @return A strictly syntactically correct JSON text. + */ + public String toJSONString(); +} diff --git a/src/main/java/org/json/JSONStringer.java b/src/main/java/org/json/JSONStringer.java new file mode 100644 index 0000000..25c2e5d --- /dev/null +++ b/src/main/java/org/json/JSONStringer.java @@ -0,0 +1,78 @@ +package org.json; + +/* +Copyright (c) 2006 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import java.io.StringWriter; + +/** + * JSONStringer provides a quick and convenient way of producing JSON text. + * The texts produced strictly conform to JSON syntax rules. No whitespace is + * added, so the results are ready for transmission or storage. Each instance of + * JSONStringer can produce one JSON text. + *

+ * A JSONStringer instance provides a value method for appending + * values to the + * text, and a key + * method for adding keys before values in objects. There are array + * and endArray methods that make and bound array values, and + * object and endObject methods which make and bound + * object values. All of these methods return the JSONWriter instance, + * permitting cascade style. For example,

+ * myString = new JSONStringer()
+ *     .object()
+ *         .key("JSON")
+ *         .value("Hello, World!")
+ *     .endObject()
+ *     .toString();
which produces the string
+ * {"JSON":"Hello, World!"}
+ *

+ * The first method called must be array or object. + * There are no methods for adding commas or colons. JSONStringer adds them for + * you. Objects and arrays can be nested up to 20 levels deep. + *

+ * This can sometimes be easier than using a JSONObject to build a string. + * @author JSON.org + * @version 2008-09-18 + */ +public class JSONStringer extends JSONWriter { + /** + * Make a fresh JSONStringer. It can be used to build one JSON text. + */ + public JSONStringer() { + super(new StringWriter()); + } + + /** + * Return the JSON text. This method is used to obtain the product of the + * JSONStringer instance. It will return null if there was a + * problem in the construction of the JSON text (such as the calls to + * array were not properly balanced with calls to + * endArray). + * @return The JSON text. + */ + public String toString() { + return this.mode == 'd' ? this.writer.toString() : null; + } +} diff --git a/src/main/java/org/json/JSONTokener.java b/src/main/java/org/json/JSONTokener.java new file mode 100644 index 0000000..fe52f31 --- /dev/null +++ b/src/main/java/org/json/JSONTokener.java @@ -0,0 +1,435 @@ +package org.json; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * A JSONTokener takes a source string and extracts characters and tokens from + * it. It is used by the JSONObject and JSONArray constructors to parse + * JSON source strings. + * @author JSON.org + * @version 2010-02-02 + */ +public class JSONTokener { + + private int character; + private boolean eof; + private int index; + private int line; + private char previous; + private Reader reader; + private boolean usePrevious; + + + /** + * Construct a JSONTokener from a reader. + * + * @param reader A reader. + */ + public JSONTokener(Reader reader) { + this.reader = reader.markSupported() ? + reader : new BufferedReader(reader); + this.eof = false; + this.usePrevious = false; + this.previous = 0; + this.index = 0; + this.character = 1; + this.line = 1; + } + + + /** + * Construct a JSONTokener from a string. + * + * @param s A source string. + */ + public JSONTokener(String s) { + this(new StringReader(s)); + } + + + /** + * Back up one character. This provides a sort of lookahead capability, + * so that you can test for a digit or letter before attempting to parse + * the next number or identifier. + */ + public void back() throws JSONException { + if (usePrevious || index <= 0) { + throw new JSONException("Stepping back two steps is not supported"); + } + this.index -= 1; + this.character -= 1; + this.usePrevious = true; + this.eof = false; + } + + + /** + * Get the hex value of a character (base16). + * @param c A character between '0' and '9' or between 'A' and 'F' or + * between 'a' and 'f'. + * @return An int between 0 and 15, or -1 if c was not a hex digit. + */ + public static int dehexchar(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'A' && c <= 'F') { + return c - ('A' - 10); + } + if (c >= 'a' && c <= 'f') { + return c - ('a' - 10); + } + return -1; + } + + public boolean end() { + return eof && !usePrevious; + } + + + /** + * Determine if the source string still contains characters that next() + * can consume. + * @return true if not yet at the end of the source. + */ + public boolean more() throws JSONException { + next(); + if (end()) { + return false; + } + back(); + return true; + } + + + /** + * Get the next character in the source string. + * + * @return The next character, or 0 if past the end of the source string. + */ + public char next() throws JSONException { + int c; + if (this.usePrevious) { + this.usePrevious = false; + c = this.previous; + } else { + try { + c = this.reader.read(); + } catch (IOException exception) { + throw new JSONException(exception); + } + + if (c <= 0) { // End of stream + this.eof = true; + c = 0; + } + } + this.index += 1; + if (this.previous == '\r') { + this.line += 1; + this.character = c == '\n' ? 0 : 1; + } else if (c == '\n') { + this.line += 1; + this.character = 0; + } else { + this.character += 1; + } + this.previous = (char) c; + return this.previous; + } + + + /** + * Consume the next character, and check that it matches a specified + * character. + * @param c The character to match. + * @return The character. + * @throws JSONException if the character does not match. + */ + public char next(char c) throws JSONException { + char n = next(); + if (n != c) { + throw syntaxError("Expected '" + c + "' and instead saw '" + + n + "'"); + } + return n; + } + + + /** + * Get the next n characters. + * + * @param n The number of characters to take. + * @return A string of n characters. + * @throws JSONException + * Substring bounds error if there are not + * n characters remaining in the source string. + */ + public String next(int n) throws JSONException { + if (n == 0) { + return ""; + } + + char[] buffer = new char[n]; + int pos = 0; + + while (pos < n) { + buffer[pos] = next(); + if (end()) { + throw syntaxError("Substring bounds error"); + } + pos += 1; + } + return new String(buffer); + } + + + /** + * Get the next char in the string, skipping whitespace. + * @throws JSONException + * @return A character, or 0 if there are no more characters. + */ + public char nextClean() throws JSONException { + for (;;) { + char c = next(); + if (c == 0 || c > ' ') { + return c; + } + } + } + + + /** + * Return the characters up to the next close quote character. + * Backslash processing is done. The formal JSON format does not + * allow strings in single quotes, but an implementation is allowed to + * accept them. + * @param quote The quoting character, either + * " (double quote) or + * ' (single quote). + * @return A String. + * @throws JSONException Unterminated string. + */ + public String nextString(char quote) throws JSONException { + char c; + StringBuffer sb = new StringBuffer(); + for (;;) { + c = next(); + switch (c) { + case 0: + case '\n': + case '\r': + throw syntaxError("Unterminated string"); + case '\\': + c = next(); + switch (c) { + case 'b': + sb.append('\b'); + break; + case 't': + sb.append('\t'); + break; + case 'n': + sb.append('\n'); + break; + case 'f': + sb.append('\f'); + break; + case 'r': + sb.append('\r'); + break; + case 'u': + sb.append((char)Integer.parseInt(next(4), 16)); + break; + case '"': + case '\'': + case '\\': + case '/': + sb.append(c); + break; + default: + throw syntaxError("Illegal escape."); + } + break; + default: + if (c == quote) { + return sb.toString(); + } + sb.append(c); + } + } + } + + + /** + * Get the text up but not including the specified character or the + * end of line, whichever comes first. + * @param d A delimiter character. + * @return A string. + */ + public String nextTo(char d) throws JSONException { + StringBuffer sb = new StringBuffer(); + for (;;) { + char c = next(); + if (c == d || c == 0 || c == '\n' || c == '\r') { + if (c != 0) { + back(); + } + return sb.toString().trim(); + } + sb.append(c); + } + } + + + /** + * Get the text up but not including one of the specified delimiter + * characters or the end of line, whichever comes first. + * @param delimiters A set of delimiter characters. + * @return A string, trimmed. + */ + public String nextTo(String delimiters) throws JSONException { + char c; + StringBuffer sb = new StringBuffer(); + for (;;) { + c = next(); + if (delimiters.indexOf(c) >= 0 || c == 0 || + c == '\n' || c == '\r') { + if (c != 0) { + back(); + } + return sb.toString().trim(); + } + sb.append(c); + } + } + + + /** + * Get the next value. The value can be a Boolean, Double, Integer, + * JSONArray, JSONObject, Long, or String, or the JSONObject.NULL object. + * @throws JSONException If syntax error. + * + * @return An object. + */ + public Object nextValue() throws JSONException { + char c = nextClean(); + String s; + + switch (c) { + case '"': + case '\'': + return nextString(c); + case '{': + back(); + return new JSONObject(this); + case '[': + case '(': + back(); + return new JSONArray(this); + } + + /* + * Handle unquoted text. This could be the values true, false, or + * null, or it can be a number. An implementation (such as this one) + * is allowed to also accept non-standard forms. + * + * Accumulate characters until we reach the end of the text or a + * formatting character. + */ + + StringBuffer sb = new StringBuffer(); + while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) { + sb.append(c); + c = next(); + } + back(); + + s = sb.toString().trim(); + if (s.equals("")) { + throw syntaxError("Missing value"); + } + return JSONObject.stringToValue(s); + } + + + /** + * Skip characters until the next character is the requested character. + * If the requested character is not found, no characters are skipped. + * @param to A character to skip to. + * @return The requested character, or zero if the requested character + * is not found. + */ + public char skipTo(char to) throws JSONException { + char c; + try { + int startIndex = this.index; + int startCharacter = this.character; + int startLine = this.line; + reader.mark(Integer.MAX_VALUE); + do { + c = next(); + if (c == 0) { + reader.reset(); + this.index = startIndex; + this.character = startCharacter; + this.line = startLine; + return c; + } + } while (c != to); + } catch (IOException exc) { + throw new JSONException(exc); + } + + back(); + return c; + } + + + /** + * Make a JSONException to signal a syntax error. + * + * @param message The error message. + * @return A JSONException object, suitable for throwing + */ + public JSONException syntaxError(String message) { + return new JSONException(message + toString()); + } + + + /** + * Make a printable string of this JSONTokener. + * + * @return " at {index} [character {character} line {line}]" + */ + public String toString() { + return " at " + index + " [character " + this.character + " line " + this.line + "]"; + } +} \ No newline at end of file diff --git a/src/main/java/org/json/JSONWriter.java b/src/main/java/org/json/JSONWriter.java new file mode 100644 index 0000000..3622a5b --- /dev/null +++ b/src/main/java/org/json/JSONWriter.java @@ -0,0 +1,323 @@ +package org.json; + +import java.io.IOException; +import java.io.Writer; + +/* +Copyright (c) 2006 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * JSONWriter provides a quick and convenient way of producing JSON text. + * The texts produced strictly conform to JSON syntax rules. No whitespace is + * added, so the results are ready for transmission or storage. Each instance of + * JSONWriter can produce one JSON text. + *

+ * A JSONWriter instance provides a value method for appending + * values to the + * text, and a key + * method for adding keys before values in objects. There are array + * and endArray methods that make and bound array values, and + * object and endObject methods which make and bound + * object values. All of these methods return the JSONWriter instance, + * permitting a cascade style. For example,

+ * new JSONWriter(myWriter)
+ *     .object()
+ *         .key("JSON")
+ *         .value("Hello, World!")
+ *     .endObject();
which writes
+ * {"JSON":"Hello, World!"}
+ *

+ * The first method called must be array or object. + * There are no methods for adding commas or colons. JSONWriter adds them for + * you. Objects and arrays can be nested up to 20 levels deep. + *

+ * This can sometimes be easier than using a JSONObject to build a string. + * @author JSON.org + * @version 2010-03-11 + */ +public class JSONWriter { + private static final int maxdepth = 20; + + /** + * The comma flag determines if a comma should be output before the next + * value. + */ + private boolean comma; + + /** + * The current mode. Values: + * 'a' (array), + * 'd' (done), + * 'i' (initial), + * 'k' (key), + * 'o' (object). + */ + protected char mode; + + /** + * The object/array stack. + */ + private JSONObject stack[]; + + /** + * The stack top index. A value of 0 indicates that the stack is empty. + */ + private int top; + + /** + * The writer that will receive the output. + */ + protected Writer writer; + + /** + * Make a fresh JSONWriter. It can be used to build one JSON text. + */ + public JSONWriter(Writer w) { + this.comma = false; + this.mode = 'i'; + this.stack = new JSONObject[maxdepth]; + this.top = 0; + this.writer = w; + } + + /** + * Append a value. + * @param s A string value. + * @return this + * @throws JSONException If the value is out of sequence. + */ + private JSONWriter append(String s) throws JSONException { + if (s == null) { + throw new JSONException("Null pointer"); + } + if (this.mode == 'o' || this.mode == 'a') { + try { + if (this.comma && this.mode == 'a') { + this.writer.write(','); + } + this.writer.write(s); + } catch (IOException e) { + throw new JSONException(e); + } + if (this.mode == 'o') { + this.mode = 'k'; + } + this.comma = true; + return this; + } + throw new JSONException("Value out of sequence."); + } + + /** + * Begin appending a new array. All values until the balancing + * endArray will be appended to this array. The + * endArray method must be called to mark the array's end. + * @return this + * @throws JSONException If the nesting is too deep, or if the object is + * started in the wrong place (for example as a key or after the end of the + * outermost array or object). + */ + public JSONWriter array() throws JSONException { + if (this.mode == 'i' || this.mode == 'o' || this.mode == 'a') { + this.push(null); + this.append("["); + this.comma = false; + return this; + } + throw new JSONException("Misplaced array."); + } + + /** + * End something. + * @param m Mode + * @param c Closing character + * @return this + * @throws JSONException If unbalanced. + */ + private JSONWriter end(char m, char c) throws JSONException { + if (this.mode != m) { + throw new JSONException(m == 'a' ? "Misplaced endArray." : + "Misplaced endObject."); + } + this.pop(m); + try { + this.writer.write(c); + } catch (IOException e) { + throw new JSONException(e); + } + this.comma = true; + return this; + } + + /** + * End an array. This method most be called to balance calls to + * array. + * @return this + * @throws JSONException If incorrectly nested. + */ + public JSONWriter endArray() throws JSONException { + return this.end('a', ']'); + } + + /** + * End an object. This method most be called to balance calls to + * object. + * @return this + * @throws JSONException If incorrectly nested. + */ + public JSONWriter endObject() throws JSONException { + return this.end('k', '}'); + } + + /** + * Append a key. The key will be associated with the next value. In an + * object, every value must be preceded by a key. + * @param s A key string. + * @return this + * @throws JSONException If the key is out of place. For example, keys + * do not belong in arrays or if the key is null. + */ + public JSONWriter key(String s) throws JSONException { + if (s == null) { + throw new JSONException("Null key."); + } + if (this.mode == 'k') { + try { + stack[top - 1].putOnce(s, Boolean.TRUE); + if (this.comma) { + this.writer.write(','); + } + this.writer.write(JSONObject.quote(s)); + this.writer.write(':'); + this.comma = false; + this.mode = 'o'; + return this; + } catch (IOException e) { + throw new JSONException(e); + } + } + throw new JSONException("Misplaced key."); + } + + + /** + * Begin appending a new object. All keys and values until the balancing + * endObject will be appended to this object. The + * endObject method must be called to mark the object's end. + * @return this + * @throws JSONException If the nesting is too deep, or if the object is + * started in the wrong place (for example as a key or after the end of the + * outermost array or object). + */ + public JSONWriter object() throws JSONException { + if (this.mode == 'i') { + this.mode = 'o'; + } + if (this.mode == 'o' || this.mode == 'a') { + this.append("{"); + this.push(new JSONObject()); + this.comma = false; + return this; + } + throw new JSONException("Misplaced object."); + + } + + + /** + * Pop an array or object scope. + * @param c The scope to close. + * @throws JSONException If nesting is wrong. + */ + private void pop(char c) throws JSONException { + if (this.top <= 0) { + throw new JSONException("Nesting error."); + } + char m = this.stack[this.top - 1] == null ? 'a' : 'k'; + if (m != c) { + throw new JSONException("Nesting error."); + } + this.top -= 1; + this.mode = this.top == 0 ? 'd' : this.stack[this.top - 1] == null ? 'a' : 'k'; + } + + /** + * Push an array or object scope. + * @param c The scope to open. + * @throws JSONException If nesting is too deep. + */ + private void push(JSONObject jo) throws JSONException { + if (this.top >= maxdepth) { + throw new JSONException("Nesting too deep."); + } + this.stack[this.top] = jo; + this.mode = jo == null ? 'a' : 'k'; + this.top += 1; + } + + + /** + * Append either the value true or the value + * false. + * @param b A boolean. + * @return this + * @throws JSONException + */ + public JSONWriter value(boolean b) throws JSONException { + return this.append(b ? "true" : "false"); + } + + /** + * Append a double value. + * @param d A double. + * @return this + * @throws JSONException If the number is not finite. + */ + public JSONWriter value(double d) throws JSONException { + return this.value(new Double(d)); + } + + /** + * Append a long value. + * @param l A long. + * @return this + * @throws JSONException + */ + public JSONWriter value(long l) throws JSONException { + return this.append(Long.toString(l)); + } + + + /** + * Append an object value. + * @param o The object to append. It can be null, or a Boolean, Number, + * String, JSONObject, or JSONArray, or an object with a toJSONString() + * method. + * @return this + * @throws JSONException If the value is out of sequence. + */ + public JSONWriter value(Object o) throws JSONException { + return this.append(JSONObject.valueToString(o)); + } +} diff --git a/src/main/java/org/json/XML.java b/src/main/java/org/json/XML.java new file mode 100644 index 0000000..bef4693 --- /dev/null +++ b/src/main/java/org/json/XML.java @@ -0,0 +1,441 @@ +package org.json; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import java.util.Iterator; + + +/** + * This provides static methods to convert an XML text into a JSONObject, + * and to covert a JSONObject into an XML text. + * @author JSON.org + * @version 2010-04-08 + */ +public class XML { + + /** The Character '&'. */ + public static final Character AMP = new Character('&'); + + /** The Character '''. */ + public static final Character APOS = new Character('\''); + + /** The Character '!'. */ + public static final Character BANG = new Character('!'); + + /** The Character '='. */ + public static final Character EQ = new Character('='); + + /** The Character '>'. */ + public static final Character GT = new Character('>'); + + /** The Character '<'. */ + public static final Character LT = new Character('<'); + + /** The Character '?'. */ + public static final Character QUEST = new Character('?'); + + /** The Character '"'. */ + public static final Character QUOT = new Character('"'); + + /** The Character '/'. */ + public static final Character SLASH = new Character('/'); + + /** + * Replace special characters with XML escapes: + *

+     * & (ampersand) is replaced by &amp;
+     * < (less than) is replaced by &lt;
+     * > (greater than) is replaced by &gt;
+     * " (double quote) is replaced by &quot;
+     * 
+ * @param string The string to be escaped. + * @return The escaped string. + */ + public static String escape(String string) { + StringBuffer sb = new StringBuffer(); + for (int i = 0, len = string.length(); i < len; i++) { + char c = string.charAt(i); + switch (c) { + case '&': + sb.append("&"); + break; + case '<': + sb.append("<"); + break; + case '>': + sb.append(">"); + break; + case '"': + sb.append("""); + break; + default: + sb.append(c); + } + } + return sb.toString(); + } + + /** + * Throw an exception if the string contains whitespace. + * Whitespace is not allowed in tagNames and attributes. + * @param string + * @throws JSONException + */ + public static void noSpace(String string) throws JSONException { + int i, length = string.length(); + if (length == 0) { + throw new JSONException("Empty string."); + } + for (i = 0; i < length; i += 1) { + if (Character.isWhitespace(string.charAt(i))) { + throw new JSONException("'" + string + + "' contains a space character."); + } + } + } + + /** + * Scan the content following the named tag, attaching it to the context. + * @param x The XMLTokener containing the source string. + * @param context The JSONObject that will include the new material. + * @param name The tag name. + * @return true if the close tag is processed. + * @throws JSONException + */ + private static boolean parse(XMLTokener x, JSONObject context, + String name) throws JSONException { + char c; + int i; + String n; + JSONObject o = null; + String s; + Object t; + +// Test for and skip past these forms: +// +// +// +// +// Report errors for these forms: +// <> +// <= +// << + + t = x.nextToken(); + +// "); + return false; + } + x.back(); + } else if (c == '[') { + t = x.nextToken(); + if (t.equals("CDATA")) { + if (x.next() == '[') { + s = x.nextCDATA(); + if (s.length() > 0) { + context.accumulate("content", s); + } + return false; + } + } + throw x.syntaxError("Expected 'CDATA['"); + } + i = 1; + do { + t = x.nextMeta(); + if (t == null) { + throw x.syntaxError("Missing '>' after ' 0); + return false; + } else if (t == QUEST) { + +// "); + return false; + } else if (t == SLASH) { + +// Close tag + + } else if (t == SLASH) { + if (x.nextToken() != GT) { + throw x.syntaxError("Misshaped tag"); + } + if (o.length() > 0) { + context.accumulate(n, o); + } else { + context.accumulate(n, ""); + } + return false; + +// Content, between <...> and + + } else if (t == GT) { + for (;;) { + t = x.nextContent(); + if (t == null) { + if (n != null) { + throw x.syntaxError("Unclosed tag " + n); + } + return false; + } else if (t instanceof String) { + s = (String)t; + if (s.length() > 0) { + o.accumulate("content", JSONObject.stringToValue(s)); + } + +// Nested element + + } else if (t == LT) { + if (parse(x, o, n)) { + if (o.length() == 0) { + context.accumulate(n, ""); + } else if (o.length() == 1 && + o.opt("content") != null) { + context.accumulate(n, o.opt("content")); + } else { + context.accumulate(n, o); + } + return false; + } + } + } + } else { + throw x.syntaxError("Misshaped tag"); + } + } + } + } + + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONObject. Some information may be lost in this transformation + * because JSON is a data format and XML is a document format. XML uses + * elements, attributes, and content text, while JSON uses unordered + * collections of name/value pairs and arrays of values. JSON does not + * does not like to distinguish between elements and attributes. + * Sequences of similar elements are represented as JSONArrays. Content + * text may be placed in a "content" member. Comments, prologs, DTDs, and + * <[ [ ]]> are ignored. + * @param string The source string. + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException + */ + public static JSONObject toJSONObject(String string) throws JSONException { + JSONObject o = new JSONObject(); + XMLTokener x = new XMLTokener(string); + while (x.more() && x.skipPast("<")) { + parse(x, o, null); + } + return o; + } + + + /** + * Convert a JSONObject into a well-formed, element-normal XML string. + * @param o A JSONObject. + * @return A string. + * @throws JSONException + */ + public static String toString(Object o) throws JSONException { + return toString(o, null); + } + + + /** + * Convert a JSONObject into a well-formed, element-normal XML string. + * @param o A JSONObject. + * @param tagName The optional name of the enclosing tag. + * @return A string. + * @throws JSONException + */ + public static String toString(Object o, String tagName) + throws JSONException { + StringBuffer b = new StringBuffer(); + int i; + JSONArray ja; + JSONObject jo; + String k; + Iterator keys; + int len; + String s; + Object v; + if (o instanceof JSONObject) { + +// Emit + + if (tagName != null) { + b.append('<'); + b.append(tagName); + b.append('>'); + } + +// Loop thru the keys. + + jo = (JSONObject)o; + keys = jo.keys(); + while (keys.hasNext()) { + k = keys.next().toString(); + v = jo.opt(k); + if (v == null) { + v = ""; + } + if (v instanceof String) { + s = (String)v; + } else { + s = null; + } + +// Emit content in body + + if (k.equals("content")) { + if (v instanceof JSONArray) { + ja = (JSONArray)v; + len = ja.length(); + for (i = 0; i < len; i += 1) { + if (i > 0) { + b.append('\n'); + } + b.append(escape(ja.get(i).toString())); + } + } else { + b.append(escape(v.toString())); + } + +// Emit an array of similar keys + + } else if (v instanceof JSONArray) { + ja = (JSONArray)v; + len = ja.length(); + for (i = 0; i < len; i += 1) { + v = ja.get(i); + if (v instanceof JSONArray) { + b.append('<'); + b.append(k); + b.append('>'); + b.append(toString(v)); + b.append("'); + } else { + b.append(toString(v, k)); + } + } + } else if (v.equals("")) { + b.append('<'); + b.append(k); + b.append("/>"); + +// Emit a new tag + + } else { + b.append(toString(v, k)); + } + } + if (tagName != null) { + +// Emit the close tag + + b.append("'); + } + return b.toString(); + +// XML does not have good support for arrays. If an array appears in a place +// where XML is lacking, synthesize an element. + + } else if (o instanceof JSONArray) { + ja = (JSONArray)o; + len = ja.length(); + for (i = 0; i < len; ++i) { + v = ja.opt(i); + b.append(toString(v, (tagName == null) ? "array" : tagName)); + } + return b.toString(); + } else { + s = (o == null) ? "null" : escape(o.toString()); + return (tagName == null) ? "\"" + s + "\"" : + (s.length() == 0) ? "<" + tagName + "/>" : + "<" + tagName + ">" + s + ""; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/json/XMLTokener.java b/src/main/java/org/json/XMLTokener.java new file mode 100644 index 0000000..47ff3f2 --- /dev/null +++ b/src/main/java/org/json/XMLTokener.java @@ -0,0 +1,365 @@ +package org.json; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * The XMLTokener extends the JSONTokener to provide additional methods + * for the parsing of XML texts. + * @author JSON.org + * @version 2010-01-30 + */ +public class XMLTokener extends JSONTokener { + + + /** The table of entity values. It initially contains Character values for + * amp, apos, gt, lt, quot. + */ + public static final java.util.HashMap entity; + + static { + entity = new java.util.HashMap(8); + entity.put("amp", XML.AMP); + entity.put("apos", XML.APOS); + entity.put("gt", XML.GT); + entity.put("lt", XML.LT); + entity.put("quot", XML.QUOT); + } + + /** + * Construct an XMLTokener from a string. + * @param s A source string. + */ + public XMLTokener(String s) { + super(s); + } + + /** + * Get the text in the CDATA block. + * @return The string up to the ]]>. + * @throws JSONException If the ]]> is not found. + */ + public String nextCDATA() throws JSONException { + char c; + int i; + StringBuffer sb = new StringBuffer(); + for (;;) { + c = next(); + if (end()) { + throw syntaxError("Unclosed CDATA"); + } + sb.append(c); + i = sb.length() - 3; + if (i >= 0 && sb.charAt(i) == ']' && + sb.charAt(i + 1) == ']' && sb.charAt(i + 2) == '>') { + sb.setLength(i); + return sb.toString(); + } + } + } + + + /** + * Get the next XML outer token, trimming whitespace. There are two kinds + * of tokens: the '<' character which begins a markup tag, and the content + * text between markup tags. + * + * @return A string, or a '<' Character, or null if there is no more + * source text. + * @throws JSONException + */ + public Object nextContent() throws JSONException { + char c; + StringBuffer sb; + do { + c = next(); + } while (Character.isWhitespace(c)); + if (c == 0) { + return null; + } + if (c == '<') { + return XML.LT; + } + sb = new StringBuffer(); + for (;;) { + if (c == '<' || c == 0) { + back(); + return sb.toString().trim(); + } + if (c == '&') { + sb.append(nextEntity(c)); + } else { + sb.append(c); + } + c = next(); + } + } + + + /** + * Return the next entity. These entities are translated to Characters: + * & ' > < ". + * @param a An ampersand character. + * @return A Character or an entity String if the entity is not recognized. + * @throws JSONException If missing ';' in XML entity. + */ + public Object nextEntity(char a) throws JSONException { + StringBuffer sb = new StringBuffer(); + for (;;) { + char c = next(); + if (Character.isLetterOrDigit(c) || c == '#') { + sb.append(Character.toLowerCase(c)); + } else if (c == ';') { + break; + } else { + throw syntaxError("Missing ';' in XML entity: &" + sb); + } + } + String s = sb.toString(); + Object e = entity.get(s); + return e != null ? e : a + s + ";"; + } + + + /** + * Returns the next XML meta token. This is used for skipping over + * and structures. + * @return Syntax characters (< > / = ! ?) are returned as + * Character, and strings and names are returned as Boolean. We don't care + * what the values actually are. + * @throws JSONException If a string is not properly closed or if the XML + * is badly structured. + */ + public Object nextMeta() throws JSONException { + char c; + char q; + do { + c = next(); + } while (Character.isWhitespace(c)); + switch (c) { + case 0: + throw syntaxError("Misshaped meta tag"); + case '<': + return XML.LT; + case '>': + return XML.GT; + case '/': + return XML.SLASH; + case '=': + return XML.EQ; + case '!': + return XML.BANG; + case '?': + return XML.QUEST; + case '"': + case '\'': + q = c; + for (;;) { + c = next(); + if (c == 0) { + throw syntaxError("Unterminated string"); + } + if (c == q) { + return Boolean.TRUE; + } + } + default: + for (;;) { + c = next(); + if (Character.isWhitespace(c)) { + return Boolean.TRUE; + } + switch (c) { + case 0: + case '<': + case '>': + case '/': + case '=': + case '!': + case '?': + case '"': + case '\'': + back(); + return Boolean.TRUE; + } + } + } + } + + + /** + * Get the next XML Token. These tokens are found inside of angle + * brackets. It may be one of these characters: / > = ! ? or it + * may be a string wrapped in single quotes or double quotes, or it may be a + * name. + * @return a String or a Character. + * @throws JSONException If the XML is not well formed. + */ + public Object nextToken() throws JSONException { + char c; + char q; + StringBuffer sb; + do { + c = next(); + } while (Character.isWhitespace(c)); + switch (c) { + case 0: + throw syntaxError("Misshaped element"); + case '<': + throw syntaxError("Misplaced '<'"); + case '>': + return XML.GT; + case '/': + return XML.SLASH; + case '=': + return XML.EQ; + case '!': + return XML.BANG; + case '?': + return XML.QUEST; + +// Quoted string + + case '"': + case '\'': + q = c; + sb = new StringBuffer(); + for (;;) { + c = next(); + if (c == 0) { + throw syntaxError("Unterminated string"); + } + if (c == q) { + return sb.toString(); + } + if (c == '&') { + sb.append(nextEntity(c)); + } else { + sb.append(c); + } + } + default: + +// Name + + sb = new StringBuffer(); + for (;;) { + sb.append(c); + c = next(); + if (Character.isWhitespace(c)) { + return sb.toString(); + } + switch (c) { + case 0: + return sb.toString(); + case '>': + case '/': + case '=': + case '!': + case '?': + case '[': + case ']': + back(); + return sb.toString(); + case '<': + case '"': + case '\'': + throw syntaxError("Bad character in a name"); + } + } + } + } + + + /** + * Skip characters until past the requested string. + * If it is not found, we are left at the end of the source with a result of false. + * @param to A string to skip past. + * @throws JSONException + */ + public boolean skipPast(String to) throws JSONException { + boolean b; + char c; + int i; + int j; + int offset = 0; + int n = to.length(); + char[] circle = new char[n]; + + /* + * First fill the circle buffer with as many characters as are in the + * to string. If we reach an early end, bail. + */ + + for (i = 0; i < n; i += 1) { + c = next(); + if (c == 0) { + return false; + } + circle[i] = c; + } + /* + * We will loop, possibly for all of the remaining characters. + */ + for (;;) { + j = offset; + b = true; + /* + * Compare the circle buffer with the to string. + */ + for (i = 0; i < n; i += 1) { + if (circle[j] != to.charAt(i)) { + b = false; + break; + } + j += 1; + if (j >= n) { + j -= n; + } + } + /* + * If we exit the loop with b intact, then victory is ours. + */ + if (b) { + return true; + } + /* + * Get the next character. If there isn't one, then defeat is ours. + */ + c = next(); + if (c == 0) { + return false; + } + /* + * Shove the character in the circle buffer and advance the + * circle offset. The offset is mod n. + */ + circle[offset] = c; + offset += 1; + if (offset >= n) { + offset -= n; + } + } + } +}