├── .gitignore ├── BappDescription.html ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── BappManifest.bmf ├── README.md ├── LICENSE.txt ├── gradlew.bat ├── gradlew └── src ├── burp └── BurpExtender.java └── mjson └── Json.java /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | build/ 3 | -------------------------------------------------------------------------------- /BappDescription.html: -------------------------------------------------------------------------------- 1 |

This extension copies selected request(s) as Python-Requests invocations.

2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silentsignal/burp-requests/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /BappManifest.bmf: -------------------------------------------------------------------------------- 1 | Uuid: b324647b6efa4b6a8f346389730df160 2 | ExtensionType: 1 3 | Name: Copy As Python-Requests 4 | RepoName: copy-as-python-requests 5 | ScreenVersion: 0.2.6 6 | SerialVersion: 9 7 | MinPlatformVersion: 0 8 | ProOnly: False 9 | Author: Andras Veres-Szentkiralyi 10 | ShortDescription: Copies selected request(s) as Python-Requests invocations. 11 | EntryPoint: build/libs/copy-as-python-requests.jar 12 | BuildCommand: ./gradlew jar 13 | SupportedProducts: Pro, Community 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Copy as requests plugin for Burp Suite 2 | ====================================== 3 | 4 | Copies selected request(s) as Python [requests][1] invocation. 5 | 6 | Building 7 | -------- 8 | 9 | Execute `./gradlew build` and you'll have the plugin ready in `build/libs/burp-requests.jar` 10 | 11 | License 12 | ------- 13 | 14 | The whole project is available under MIT license, see `LICENSE.txt`, 15 | except for the [Mjson library][2], where 16 | 17 | > The source code is a single Java file. [...] Some of it was ripped 18 | > off from other projects and credit and licensing notices are included 19 | > in the appropriate places. The license is Apache 2.0. 20 | 21 | [1]: http://docs.python-requests.org/ 22 | [2]: https://bolerio.github.io/mjson/ 23 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Andras Veres-Szentkiralyi 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /src/burp/BurpExtender.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import java.util.*; 4 | import java.awt.datatransfer.*; 5 | import java.awt.event.*; 6 | import java.awt.Toolkit; 7 | import java.io.UnsupportedEncodingException; 8 | import javax.swing.JMenuItem; 9 | 10 | import mjson.Json; 11 | 12 | public class BurpExtender implements IBurpExtender, IContextMenuFactory, ClipboardOwner 13 | { 14 | private IExtensionHelpers helpers; 15 | 16 | private final static String NAME = "Copy as requests"; 17 | private final static String SESSION_MENU_ITEM = NAME + " with session object"; 18 | private final static String[] PYTHON_ESCAPE = new String[256]; 19 | private final static String SESSION_VAR = "session"; 20 | 21 | static { 22 | for (int i = 0x00; i <= 0xFF; i++) PYTHON_ESCAPE[i] = String.format("\\x%02x", i); 23 | for (int i = 0x20; i < 0x80; i++) PYTHON_ESCAPE[i] = String.valueOf((char)i); 24 | PYTHON_ESCAPE['\n'] = "\\n"; 25 | PYTHON_ESCAPE['\r'] = "\\r"; 26 | PYTHON_ESCAPE['\t'] = "\\t"; 27 | PYTHON_ESCAPE['"'] = "\\\""; 28 | PYTHON_ESCAPE['\\'] = "\\\\"; 29 | } 30 | 31 | @Override 32 | public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks) 33 | { 34 | helpers = callbacks.getHelpers(); 35 | callbacks.setExtensionName(NAME); 36 | callbacks.registerContextMenuFactory(this); 37 | } 38 | 39 | @Override 40 | public List createMenuItems(IContextMenuInvocation invocation) { 41 | final IHttpRequestResponse[] messages = invocation.getSelectedMessages(); 42 | if (messages == null || messages.length == 0) return null; 43 | JMenuItem i1 = new JMenuItem(NAME); 44 | i1.addActionListener(new ActionListener() { 45 | @Override 46 | public void actionPerformed(ActionEvent e) { 47 | copyMessages(messages, false); 48 | } 49 | }); 50 | JMenuItem i2 = new JMenuItem(SESSION_MENU_ITEM); 51 | i2.addActionListener(new ActionListener() { 52 | @Override 53 | public void actionPerformed(ActionEvent e) { 54 | copyMessages(messages, true); 55 | } 56 | }); 57 | return Arrays.asList(i1, i2); 58 | } 59 | 60 | private enum BodyType {JSON, DATA}; 61 | 62 | private void copyMessages(IHttpRequestResponse[] messages, boolean withSessionObject) { 63 | StringBuilder py = new StringBuilder("import requests"); 64 | String requestsMethodPrefix = 65 | "\n" + (withSessionObject ? SESSION_VAR : "requests") + "."; 66 | int i = 0; 67 | 68 | if (withSessionObject) { 69 | py.append("\n\n" + SESSION_VAR + " = requests.session()"); 70 | } 71 | 72 | for (IHttpRequestResponse message : messages) { 73 | IRequestInfo ri = helpers.analyzeRequest(message); 74 | byte[] req = message.getRequest(); 75 | String prefix = "burp" + i++ + "_"; 76 | py.append("\n\n").append(prefix).append("url = \""); 77 | py.append(escapeQuotes(ri.getUrl().toString())); 78 | py.append('"'); 79 | List headers = ri.getHeaders(); 80 | boolean cookiesExist = processCookies(prefix, py, headers); 81 | py.append('\n').append(prefix).append("headers = {"); 82 | processHeaders(py, headers); 83 | py.append('}'); 84 | BodyType bodyType = processBody(prefix, py, req, ri); 85 | py.append(requestsMethodPrefix); 86 | py.append(ri.getMethod().toLowerCase()); 87 | py.append('(').append(prefix).append("url, headers="); 88 | py.append(prefix).append("headers"); 89 | if (cookiesExist) py.append(", cookies=").append(prefix).append("cookies"); 90 | if (bodyType != null) { 91 | String kind = bodyType.toString().toLowerCase(); 92 | py.append(", ").append(kind).append('=').append(prefix).append(kind); 93 | } 94 | py.append(')'); 95 | } 96 | 97 | Toolkit.getDefaultToolkit().getSystemClipboard() 98 | .setContents(new StringSelection(py.toString()), this); 99 | } 100 | 101 | private static boolean processCookies(String prefix, StringBuilder py, 102 | List headers) { 103 | ListIterator iter = headers.listIterator(); 104 | boolean cookiesExist = false; 105 | outer: 106 | while (iter.hasNext()) { 107 | String header = iter.next(); 108 | if (!header.toLowerCase().startsWith("cookie:")) continue; 109 | for (String cookie : header.substring(8).split("; ?")) { 110 | String[] parts = cookie.split("=", 2); 111 | if (parts.length < 2) continue outer; 112 | if (cookiesExist) { 113 | py.append(", \""); 114 | } else { 115 | cookiesExist = true; 116 | py.append('\n').append(prefix).append("cookies = {\""); 117 | } 118 | py.append(escapeQuotes(parts[0])); 119 | py.append("\": \""); 120 | py.append(escapeQuotes(parts[1])); 121 | py.append('"'); 122 | } 123 | iter.remove(); 124 | } 125 | if (cookiesExist) py.append('}'); 126 | return cookiesExist; 127 | } 128 | 129 | private static final Collection IGNORE_HEADERS = Arrays.asList("host:", "content-length:"); 130 | 131 | private static void processHeaders(StringBuilder py, List headers) { 132 | boolean firstHeader = true; 133 | boolean requestLine = true; 134 | header_loop: 135 | for (String header : headers) { 136 | if (requestLine) { 137 | requestLine = false; 138 | continue; 139 | } 140 | String lowerCaseHeader = header.toLowerCase(); 141 | for (String headerToIgnore : IGNORE_HEADERS) { 142 | if (lowerCaseHeader.startsWith(headerToIgnore)) continue header_loop; 143 | } 144 | header = escapeQuotes(header); 145 | int colonPos = header.indexOf(':'); 146 | if (colonPos == -1) continue; 147 | if (firstHeader) { 148 | firstHeader = false; 149 | py.append('"'); 150 | } else { 151 | py.append(", \""); 152 | } 153 | py.append(header, 0, colonPos); 154 | py.append("\": \""); 155 | py.append(header, colonPos + 2, header.length()); 156 | py.append('"'); 157 | } 158 | } 159 | 160 | private BodyType processBody(String prefix, StringBuilder py, 161 | byte[] req, IRequestInfo ri) { 162 | int bo = ri.getBodyOffset(); 163 | if (bo >= req.length - 2) return null; 164 | py.append('\n').append(prefix); 165 | byte contentType = ri.getContentType(); 166 | if (contentType == IRequestInfo.CONTENT_TYPE_JSON) { 167 | try { 168 | Json root = Json.read(byteSliceToString(req, bo, req.length)); 169 | py.append("json="); 170 | escapeJson(root, py); 171 | return BodyType.JSON; 172 | } catch (Exception e) { 173 | // not valid JSON, treat it like any other kind of data 174 | } 175 | } 176 | py.append("data = "); 177 | if (contentType == IRequestInfo.CONTENT_TYPE_URL_ENCODED) { 178 | py.append('{'); 179 | boolean firstKey = true; 180 | int keyStart = bo, keyEnd = -1; 181 | for (int pos = bo; pos < req.length; pos++) { 182 | byte b = req[pos]; 183 | if (keyEnd == -1) { 184 | if (b == (byte)'=') { 185 | if (pos == req.length - 1) { 186 | if (!firstKey) py.append(", "); 187 | escapeUrlEncodedBytes(req, py, keyStart, pos); 188 | py.append(": ''"); 189 | } else { 190 | keyEnd = pos; 191 | } 192 | } 193 | } else if (b == (byte)'&' || pos == req.length - 1) { 194 | if (firstKey) firstKey = false; else py.append(", "); 195 | escapeUrlEncodedBytes(req, py, keyStart, keyEnd); 196 | py.append(": "); 197 | escapeUrlEncodedBytes(req, py, keyEnd + 1, 198 | pos == req.length - 1 ? req.length : pos); 199 | keyEnd = -1; 200 | keyStart = pos + 1; 201 | } 202 | } 203 | py.append('}'); 204 | } else { 205 | escapeBytes(req, py, bo, req.length); 206 | } 207 | return BodyType.DATA; 208 | } 209 | 210 | private static String escapeQuotes(String value) { 211 | return value.replace("\\", "\\\\").replace("\"", "\\\"") 212 | .replace("\n", "\\n").replace("\r", "\\r"); 213 | } 214 | 215 | private void escapeUrlEncodedBytes(byte[] input, StringBuilder output, 216 | int start, int end) { 217 | if (end > start) { 218 | byte[] dec = helpers.urlDecode(Arrays.copyOfRange(input, start, end)); 219 | escapeBytes(dec, output, 0, dec.length); 220 | } else { 221 | output.append("''"); 222 | } 223 | } 224 | 225 | private static final String PYTHON_TRUE = "True", PYTHON_FALSE = "False", PYTHON_NULL = "None"; 226 | 227 | private static void escapeJson(Json node, StringBuilder output) { 228 | if (node.isObject()) { 229 | output.append('{'); 230 | Map tm = new TreeMap(String.CASE_INSENSITIVE_ORDER); 231 | tm.putAll(node.asJsonMap()); 232 | final Iterator> iter = tm.entrySet().iterator(); 233 | if (iter.hasNext()) { 234 | appendIteratedEntry(iter, output); 235 | while (iter.hasNext()) { 236 | output.append(", "); 237 | appendIteratedEntry(iter, output); 238 | } 239 | } 240 | output.append('}'); 241 | } else if (node.isArray()) { 242 | output.append('['); 243 | final Iterator iter = node.asJsonList().iterator(); 244 | if (iter.hasNext()) { 245 | escapeJson(iter.next(), output); 246 | while (iter.hasNext()) { 247 | output.append(", "); 248 | escapeJson(iter.next(), output); 249 | } 250 | } 251 | output.append(']'); 252 | } else if (node.isString()) { 253 | escapeString(node.asString(), output); 254 | } else if (node.isBoolean()) { 255 | output.append(node.asBoolean() ? PYTHON_TRUE : PYTHON_FALSE); 256 | } else if (node.isNull()) { 257 | output.append(PYTHON_NULL); 258 | } else if (node.isNumber()) { 259 | output.append(node.asString()); 260 | } 261 | } 262 | 263 | private static void appendIteratedEntry(Iterator> iter, StringBuilder output) { 264 | final Map.Entry e = iter.next(); 265 | escapeString(e.getKey(), output); 266 | output.append(": "); 267 | escapeJson(e.getValue(), output); 268 | } 269 | 270 | private static String byteSliceToString(byte[] input, int from, int till) { 271 | try { 272 | return new String(input, from, till - from, "ISO-8859-1"); 273 | } catch (UnsupportedEncodingException uee) { 274 | throw new RuntimeException("All JVMs must support ISO-8859-1"); 275 | } 276 | } 277 | 278 | private static void escapeString(String input, StringBuilder output) { 279 | output.append('"'); 280 | int length = input.length(); 281 | for (int pos = 0; pos < length; pos++) { 282 | output.append(PYTHON_ESCAPE[input.charAt(pos) & 0xFF]); 283 | } 284 | output.append('"'); 285 | } 286 | 287 | private static void escapeBytes(byte[] input, StringBuilder output, 288 | int start, int end) { 289 | output.append('"'); 290 | for (int pos = start; pos < end; pos++) { 291 | output.append(PYTHON_ESCAPE[input[pos] & 0xFF]); 292 | } 293 | output.append('"'); 294 | } 295 | 296 | @Override 297 | public void lostOwnership(Clipboard aClipboard, Transferable aContents) {} 298 | } 299 | -------------------------------------------------------------------------------- /src/mjson/Json.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Miami-Dade County. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * Note: this file incorporates source code from 3d party entities. Such code 17 | * is copyrighted by those entities as indicated below. 18 | */ 19 | package mjson; 20 | 21 | import java.io.IOException; 22 | import java.math.BigDecimal; 23 | import java.math.BigInteger; 24 | import java.net.URI; 25 | import java.net.URL; 26 | import java.text.CharacterIterator; 27 | import java.text.StringCharacterIterator; 28 | import java.util.ArrayList; 29 | import java.util.Collection; 30 | import java.util.Collections; 31 | import java.util.HashMap; 32 | import java.util.HashSet; 33 | import java.util.IdentityHashMap; 34 | import java.util.Iterator; 35 | import java.util.List; 36 | import java.util.Map; 37 | import java.util.Objects; 38 | import java.util.Set; 39 | import java.util.function.Function; 40 | import java.util.regex.Pattern; 41 | 42 | /** 43 | * 44 | *

45 | * Represents a JSON (JavaScript Object Notation) entity. For more information about JSON, please see 46 | * http://www.json.org. 47 | *

48 | * 49 | *

50 | * A JSON entity can be one of several things: an object (set of name/Json entity pairs), an array (a list of 51 | * other JSON entities), a string, a number, a boolean or null. All of those are represented as Json 52 | * instances. Each of the different types of entities supports a different set of operations. However, this class 53 | * unifies all operations into a single interface so in Java one is always dealing with a single object type: this class. 54 | * The approach effectively amounts to dynamic typing where using an unsupported operation won't be detected at 55 | * compile time, but will throw a runtime {@link UnsupportedOperationException}. It simplifies working with JSON 56 | * structures considerably and it leads to shorter at cleaner Java code. It makes much easier to work 57 | * with JSON structure without the need to convert to "proper" Java representation in the form of 58 | * POJOs and the like. When traversing a JSON, there's no need to type-cast at each step because there's 59 | * only one type: Json. 60 | *

61 | * 62 | *

63 | * One can examine the concrete type of a Json with one of the isXXX methods: 64 | * {@link #isObject()}, {@link #isArray()},{@link #isNumber()},{@link #isBoolean()},{@link #isString()}, 65 | * {@link #isNull()}. 66 | *

67 | * 68 | *

69 | * The underlying representation of a given Json instance can be obtained by calling 70 | * the generic {@link #getValue()} method or one of the asXXX methods such 71 | * as {@link #asBoolean()} or {@link #asString()} etc. 72 | * JSON objects are represented as Java {@link Map}s while JSON arrays are represented as Java 73 | * {@link List}s. Because those are mutable aggregate structures, there are two versions of the 74 | * corresponding asXXX methods: {@link #asMap()} which performs a deep copy of the underlying 75 | * map, unwrapping every nested Json entity to its Java representation and {@link #asJsonMap()} which 76 | * simply return the map reference. Similarly there are {@link #asList()} and {@link #asJsonList()}. 77 | *

78 | * 79 | *

Constructing and Modifying JSON Structures

80 | * 81 | *

82 | * There are several static factory methods in this class that allow you to create new 83 | * Json instances: 84 | *

85 | * 86 | * 87 | * 88 | * 91 | * 92 | * 93 | * 96 | * 97 | * 98 | * 99 | * 100 | * 101 | * 102 | * 103 | * 104 | * 108 | * 109 | * 110 | * 111 | * 112 | * 113 | * 114 | * 115 | *
{@link #read(String)}Parse a JSON string and return the resulting Json instance. The syntax 89 | * recognized is as defined in http://www.json.org. 90 | *
{@link #make(Object)}Creates a Json instance based on the concrete type of the parameter. The types 94 | * recognized are null, numbers, primitives, String, Map, Collection, Java arrays 95 | * and Json itself.
{@link #nil()}Return a Json instance representing JSON null.
{@link #object()}Create and return an empty JSON object.
{@link #object(Object...)}Create and return a JSON object populated with the key/value pairs 105 | * passed as an argument sequence. Each even parameter becomes a key (via 106 | * toString) and each odd parameter is converted to a Json 107 | * value.
{@link #array()}Create and return an empty JSON array.
{@link #array(Object...)}Create and return a JSON array from the list of arguments.
116 | * 117 | *

118 | * To customize how Json elements are represented and to provide your own version of the 119 | * {@link #make(Object)} method, you create an implementation of the {@link Factory} interface 120 | * and configure it either globally with the {@link #setGlobalFactory(Factory)} method or 121 | * on a per-thread basis with the {@link #attachFactory(Factory)}/{@link #detachFactory()} 122 | * methods. 123 | *

124 | * 125 | *

126 | * If a Json instance is an object, you can set its properties by 127 | * calling the {@link #set(String, Object)} method which will add a new property or replace an existing one. 128 | * Adding elements to an array Json is done with the {@link #add(Object)} method. 129 | * Removing elements by their index (or key) is done with the {@link #delAt(int)} (or 130 | * {@link #delAt(String)}) method. You can also remove an element from an array without 131 | * knowing its index with the {@link #remove(Object)} method. All these methods return the 132 | * Json instance being manipulated so that method calls can be chained. 133 | * If you want to remove an element from an object or array and return the removed element 134 | * as a result of the operation, call {@link #atDel(int)} or {@link #atDel(String)} instead. 135 | *

136 | * 137 | *

138 | * If you want to add properties to an object in bulk or append a sequence of elements to array, 139 | * use the {@link #with(Json, Json...opts)} method. When used on an object, this method expects another 140 | * object as its argument and it will copy all properties of that argument into itself. Similarly, 141 | * when called on array, the method expects another array and it will append all elements of its 142 | * argument to itself. 143 | *

144 | * 145 | *

146 | * To make a clone of a Json object, use the {@link #dup()} method. This method will create a new 147 | * object even for the immutable primitive Json types. Objects and arrays are cloned 148 | * (i.e. duplicated) recursively. 149 | *

150 | * 151 | *

Navigating JSON Structures

152 | * 153 | *

154 | * The {@link #at(int)} method returns the array element at the specified index and the 155 | * {@link #at(String)} method does the same for a property of an object instance. You can 156 | * use the {@link #at(String, Object)} version to create an object property with a default 157 | * value if it doesn't exist already. 158 | *

159 | * 160 | *

161 | * To test just whether a Json object has a given property, use the {@link #has(String)} method. To test 162 | * whether a given object property or an array elements is equal to a particular value, use the 163 | * {@link #is(String, Object)} and {@link #is(int, Object)} methods respectively. Those methods return 164 | * true if the given named property (or indexed element) is equal to the passed in Object as the second 165 | * parameter. They return false if an object doesn't have the specified property or an index array is out 166 | * of bounds. For example is(name, value) is equivalent to 'has(name) && at(name).equals(make(value))'. 167 | *

168 | * 169 | *

170 | * To help in navigating JSON structures, instances of this class contain a reference to the 171 | * enclosing JSON entity (object or array) if any. The enclosing entity can be accessed 172 | * with {@link #up()} method. 173 | *

174 | * 175 | *

176 | * The combination of method chaining when modifying Json instances and 177 | * the ability to navigate "inside" a structure and then go back to the enclosing 178 | * element lets one accomplish a lot in a single Java statement, without the need 179 | * of intermediary variables. Here for example how the following JSON structure can 180 | * be created in one statement using chained calls: 181 | *

182 | * 183 | *

 184 |  * {"menu": {
 185 |  * "id": "file",
 186 |  * "value": "File",
 187 |  * "popup": {
 188 |  *   "menuitem": [
 189 |  *     {"value": "New", "onclick": "CreateNewDoc()"},
 190 |  *     {"value": "Open", "onclick": "OpenDoc()"},
 191 |  *     {"value": "Close", "onclick": "CloseDoc()"}
 192 |  *   ]
 193 |  * }
 194 |  * "position": 0
 195 |  * }}
 196 |  * 
197 | * 198 | *

 199 |  * import mjson.Json;
 200 |  * import static mjson.Json.*;
 201 |  * ...
 202 |  * Json j = object()
 203 |  *  .at("menu", object())
 204 |  *    .set("id", "file") 
 205 |  *    .set("value", "File")
 206 |  *    .at("popup", object())
 207 |  *      .at("menuitem", array())
 208 |  *        .add(object("value", "New", "onclick", "CreateNewDoc()"))
 209 |  *        .add(object("value", "Open", "onclick", "OpenDoc()"))
 210 |  *        .add(object("value", "Close", "onclick", "CloseDoc()"))
 211 |  *        .up()
 212 |  *      .up()
 213 |  *    .set("position", 0)
 214 |  *  .up();       
 215 |  * ...
 216 |  * 
217 | * 218 | *

219 | * If there's no danger of naming conflicts, a static import of the factory methods ( 220 | * import static json.Json.*;) would reduce typing even further and make the code more 221 | * readable. 222 | *

223 | * 224 | *

Converting to String

225 | * 226 | *

227 | * To get a compact string representation, simply use the {@link #toString()} method. If you 228 | * want to wrap it in a JavaScript callback (for JSON with padding), use the {@link #pad(String)} 229 | * method. 230 | *

231 | * 232 | *

Validating with JSON Schema

233 | * 234 | *

235 | * Since version 1.3, mJson supports JSON Schema, draft 4. A schema is represented by the internal 236 | * class {@link mjson.Json.Schema}. To perform a validation, you have a instantiate a Json.Schema 237 | * using the factory method {@link mjson.Json.Schema} and then call its validate method 238 | * on a JSON instance: 239 | *

240 | * 241 | *

 242 |  * import mjson.Json;
 243 |  * import static mjson.Json.*;
 244 |  * ...
 245 |  * Json inputJson = Json.read(inputString);
 246 |  * Json schema = Json.schema(new URI("http://mycompany.com/schemas/model"));
 247 |  * Json errors = schema.validate(inputJson);
 248 |  * for (Json error : errors.asJsonList())
 249 |  * 	   System.out.println("Validation error " + err);
 250 |  * 
251 | * @author Borislav Iordanov 252 | * @version 1.4 253 | */ 254 | public class Json implements java.io.Serializable 255 | { 256 | private static final long serialVersionUID = 1L; 257 | 258 | /** 259 | *

260 | * This interface defines how Json instances are constructed. There is a 261 | * default implementation for each kind of Json value, but you can provide 262 | * your own implementation. For example, you might want a different representation of 263 | * an object than a regular HashMap. Or you might want string comparison to be 264 | * case insensitive. 265 | *

266 | * 267 | *

268 | * In addition, the {@link #make(Object)} method allows you plug-in your own mapping 269 | * of arbitrary Java objects to Json instances. You might want to implement 270 | * a Java Beans to JSON mapping or any other JSON serialization that makes sense in your 271 | * project. 272 | *

273 | * 274 | *

275 | * To avoid implementing all methods in that interface, you can extend the {@link DefaultFactory} 276 | * default implementation and simply overwrite the ones you're interested in. 277 | *

278 | * 279 | *

280 | * The factory implementation used by the Json classes is specified simply by calling 281 | * the {@link #setGlobalFactory(Factory)} method. The factory is a static, global variable by default. 282 | * If you need different factories in different areas of a single application, you may attach them 283 | * to different threads of execution using the {@link #attachFactory(Factory)}. Recall a separate 284 | * copy of static variables is made per ClassLoader, so for example in a web application context, that 285 | * global factory can be different for each web application (as Java web servers usually use a separate 286 | * class loader per application). Thread-local factories are really a provision for special cases. 287 | *

288 | * 289 | * @author Borislav Iordanov 290 | * 291 | */ 292 | public static interface Factory 293 | { 294 | /** 295 | * Construct and return an object representing JSON null. Implementations are 296 | * free to cache a return the same instance. The resulting value must return 297 | * true from isNull() and null from 298 | * getValue(). 299 | * 300 | * @return The representation of a JSON null value. 301 | */ 302 | Json nil(); 303 | 304 | /** 305 | * Construct and return a JSON boolean. The resulting value must return 306 | * true from isBoolean() and the passed 307 | * in parameter from getValue(). 308 | * @param value The boolean value. 309 | * @return A JSON with isBoolean() == true. Implementations 310 | * are free to cache and return the same instance for true and false. 311 | */ 312 | Json bool(boolean value); 313 | 314 | /** 315 | * Construct and return a JSON string. The resulting value must return 316 | * true from isString() and the passed 317 | * in parameter from getValue(). 318 | * @param value The string to wrap as a JSON value. 319 | * @return A JSON element with the given string as a value. 320 | */ 321 | Json string(String value); 322 | 323 | /** 324 | * Construct and return a JSON number. The resulting value must return 325 | * true from isNumber() and the passed 326 | * in parameter from getValue(). 327 | * 328 | * @param value The numeric value. 329 | * @return Json instance representing that value. 330 | */ 331 | Json number(Number value); 332 | 333 | /** 334 | * Construct and return a JSON object. The resulting value must return 335 | * true from isObject() and an implementation 336 | * of java.util.Map from getValue(). 337 | * 338 | * @return An empty JSON object. 339 | */ 340 | Json object(); 341 | 342 | /** 343 | * Construct and return a JSON object. The resulting value must return 344 | * true from isArray() and an implementation 345 | * of java.util.List from getValue(). 346 | * 347 | * @return An empty JSON array. 348 | */ 349 | Json array(); 350 | 351 | /** 352 | * Construct and return a JSON object. The resulting value can be of any 353 | * JSON type. The method is responsible for examining the type of its 354 | * argument and performing an appropriate mapping to a Json 355 | * instance. 356 | * 357 | * @param anything An arbitray Java object from which to construct a Json 358 | * element. 359 | * @return The newly constructed Json instance. 360 | */ 361 | Json make(Object anything); 362 | } 363 | 364 | /** 365 | *

366 | * Represents JSON schema - a specific data format that a JSON entity must 367 | * follow. The idea of a JSON schema is very similar to XML. Its main purpose 368 | * is validating input. 369 | *

370 | * 371 | *

372 | * More information about the various JSON schema specifications can be 373 | * found at http://json-schema.org. JSON Schema is an IETF draft (v4 currently) and 374 | * our implementation follows this set of specifications. A JSON schema is specified 375 | * as a JSON object that contains keywords defined by the specification. Here are 376 | * a few introductory materials: 377 | *

    378 | *
  • http://jsonary.com/documentation/json-schema/ - 379 | * a very well-written tutorial covering the whole standard
  • 380 | *
  • http://spacetelescope.github.io/understanding-json-schema/ - 381 | * online book, tutorial (Python/Ruby based)
  • 382 | *
383 | *

384 | * @author Borislav Iordanov 385 | * 386 | */ 387 | public static interface Schema 388 | { 389 | /** 390 | *

391 | * Validate a JSON document according to this schema. The validations attempts to 392 | * proceed even in the face of errors. The return value is always a Json.object 393 | * containing the boolean property ok. When ok is true, 394 | * the return object contains nothing else. When it is false, the return object 395 | * contains a property errors which is an array of error messages for all 396 | * detected schema violations. 397 | *

398 | * 399 | * @param document The input document. 400 | * @return {"ok":true} or {"ok":false, errors:["msg1", "msg2", ...]} 401 | */ 402 | Json validate(Json document); 403 | 404 | /** 405 | *

Possible options are: ignoreDefaults:true|false. 406 | *

407 | * @return A newly created Json conforming to this schema. 408 | */ 409 | //Json generate(Json options); 410 | } 411 | 412 | static String fetchContent(URL url) 413 | { 414 | java.io.Reader reader = null; 415 | try 416 | { 417 | reader = new java.io.InputStreamReader((java.io.InputStream)url.getContent()); 418 | StringBuilder content = new StringBuilder(); 419 | char [] buf = new char[1024]; 420 | for (int n = reader.read(buf); n > -1; n = reader.read(buf)) 421 | content.append(buf, 0, n); 422 | // System.out.println("last reaad: " + new StringBuilder(buf)) 423 | return content.toString(); 424 | } 425 | catch (Exception ex) 426 | { 427 | throw new RuntimeException(ex); 428 | } 429 | finally 430 | { 431 | if (reader != null) try { reader.close(); } catch (Throwable t) { } 432 | } 433 | } 434 | 435 | static Json resolvePointer(String pointerRepresentation, Json top) 436 | { 437 | String [] parts = pointerRepresentation.split("/"); 438 | Json result = top; 439 | for (String p : parts) 440 | { 441 | // TODO: unescaping and decoding 442 | if (p.length() == 0) 443 | continue; 444 | p = p.replace("~1", "/").replace("~0", "~"); 445 | if (result.isArray()) 446 | result = result.at(Integer.parseInt(p)); 447 | else if (result.isObject()) 448 | result = result.at(p); 449 | else 450 | throw new RuntimeException("Can't resolve pointer " + pointerRepresentation + 451 | " on document " + top.toString(200)); 452 | } 453 | return result; 454 | } 455 | 456 | static URI makeAbsolute(URI base, String ref) throws Exception 457 | { 458 | URI refuri; 459 | if (base != null && base.getAuthority() != null && !new URI(ref).isAbsolute()) 460 | { 461 | StringBuilder sb = new StringBuilder(); 462 | if (base.getScheme() != null) 463 | sb.append(base.getScheme()).append("://"); 464 | sb.append(base.getAuthority()); 465 | if (!ref.startsWith("/")) 466 | { 467 | if (ref.startsWith("#")) 468 | sb.append(base.getPath()); 469 | else 470 | { 471 | int slashIdx = base.getPath().lastIndexOf('/'); 472 | sb.append(slashIdx == -1 ? base.getPath() : base.getPath().substring(0, slashIdx)).append("/"); 473 | } 474 | } 475 | refuri = new URI(sb.append(ref).toString()); 476 | } 477 | else if (base != null) 478 | refuri = base.resolve(ref); 479 | else 480 | refuri = new URI(ref); 481 | return refuri; 482 | } 483 | 484 | static Json resolveRef(URI base, 485 | Json refdoc, 486 | URI refuri, 487 | Map resolved, 488 | Map expanded, 489 | Function uriResolver) throws Exception 490 | { 491 | if (refuri.isAbsolute() && 492 | (base == null || !base.isAbsolute() || 493 | !base.getScheme().equals(refuri.getScheme()) || 494 | !Objects.equals(base.getHost(), refuri.getHost()) || 495 | base.getPort() != refuri.getPort() || 496 | !base.getPath().equals(refuri.getPath()))) 497 | { 498 | URI docuri = null; 499 | refuri = refuri.normalize(); 500 | if (refuri.getHost() == null) 501 | docuri = new URI(refuri.getScheme() + ":" + refuri.getPath()); 502 | else 503 | docuri = new URI(refuri.getScheme() + "://" + refuri.getHost() + 504 | ((refuri.getPort() > -1) ? ":" + refuri.getPort() : "") + 505 | refuri.getPath()); 506 | refdoc = uriResolver.apply(docuri); 507 | refdoc = expandReferences(refdoc, refdoc, docuri, resolved, expanded, uriResolver); 508 | } 509 | if (refuri.getFragment() == null) 510 | return refdoc; 511 | else 512 | return resolvePointer(refuri.getFragment(), refdoc); 513 | } 514 | 515 | /** 516 | *

517 | * Replace all JSON references, as per the http://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03 518 | * specification, by their referants. 519 | *

520 | * @param json 521 | * @param duplicate 522 | * @param done 523 | * @return 524 | */ 525 | static Json expandReferences(Json json, 526 | Json topdoc, 527 | URI base, 528 | Map resolved, 529 | Map expanded, 530 | Function uriResolver) throws Exception 531 | { 532 | if (expanded.containsKey(json)) return json; 533 | if (json.isObject()) 534 | { 535 | if (json.has("id") && json.at("id").isString()) // change scope of nest references 536 | { 537 | base = base.resolve(json.at("id").asString()); 538 | } 539 | 540 | if (json.has("$ref")) 541 | { 542 | URI refuri = makeAbsolute(base, json.at("$ref").asString()); // base.resolve(json.at("$ref").asString()); 543 | Json ref = resolved.get(refuri.toString()); 544 | if (ref == null) 545 | { 546 | ref = resolveRef(base, topdoc, refuri, resolved, expanded, uriResolver); 547 | resolved.put(refuri.toString(), ref); 548 | ref = expandReferences(ref, topdoc, base, resolved, expanded, uriResolver); 549 | resolved.put(refuri.toString(), ref); 550 | } 551 | json = ref; 552 | } 553 | else 554 | { 555 | Json O = Json.object(); 556 | for (Map.Entry e : json.asJsonMap().entrySet()) 557 | O.set(e.getKey(), expandReferences(e.getValue(), topdoc, base, resolved, expanded, uriResolver)); 558 | json.with(O, new Json[0]); 559 | } 560 | } 561 | else if (json.isArray()) 562 | { 563 | // Json A = Json.array(); 564 | for (int i = 0; i < json.asJsonList().size(); i++) 565 | { 566 | //A.add(expandReferences(j, topdoc, base, resolved)); 567 | Json el = expandReferences(json.at(i), topdoc, base, resolved, expanded, uriResolver); 568 | json.set(i, el); 569 | } 570 | // return A; 571 | } 572 | expanded.put(json, json); 573 | return json; 574 | } 575 | 576 | static class DefaultSchema implements Schema 577 | { 578 | static interface Instruction extends Function{} 579 | 580 | static Json maybeError(Json errors, Json E) 581 | { return E == null ? errors : (errors == null ? Json.array() : errors).with(E, new Json[0]); } 582 | 583 | // Anything is valid schema 584 | static Instruction any = new Instruction() { public Json apply(Json param) { return null; } }; 585 | 586 | // Type validation 587 | class IsObject implements Instruction { public Json apply(Json param) 588 | { return param.isObject() ? null : Json.make(param.toString(maxchars)); } } 589 | class IsArray implements Instruction { public Json apply(Json param) 590 | { return param.isArray() ? null : Json.make(param.toString(maxchars)); } } 591 | class IsString implements Instruction { public Json apply(Json param) 592 | { return param.isString() ? null : Json.make(param.toString(maxchars)); } } 593 | class IsBoolean implements Instruction { public Json apply(Json param) 594 | { return param.isBoolean() ? null : Json.make(param.toString(maxchars)); } } 595 | class IsNull implements Instruction { public Json apply(Json param) 596 | { return param.isNull() ? null : Json.make(param.toString(maxchars)); } } 597 | class IsNumber implements Instruction { public Json apply(Json param) 598 | { return param.isNumber() ? null : Json.make(param.toString(maxchars)); } } 599 | class IsInteger implements Instruction { public Json apply(Json param) 600 | { return param.isNumber() && ((Number)param.getValue()) instanceof Integer ? null : Json.make(param.toString(maxchars)); } } 601 | 602 | class CheckString implements Instruction 603 | { 604 | int min = 0, max = Integer.MAX_VALUE; 605 | Pattern pattern; 606 | 607 | public Json apply(Json param) 608 | { 609 | Json errors = null; 610 | if (!param.isString()) return errors; 611 | String s = param.asString(); 612 | final int size = s.codePointCount(0, s.length()); 613 | if (size < min || size > max) 614 | errors = maybeError(errors,Json.make("String " + param.toString(maxchars) + 615 | " has length outside of the permitted range [" + min + "," + max + "].")); 616 | if (pattern != null && !pattern.matcher(s).matches()) 617 | errors = maybeError(errors,Json.make("String " + param.toString(maxchars) + 618 | " does not match regex " + pattern.toString())); 619 | return errors; 620 | } 621 | } 622 | 623 | class CheckNumber implements Instruction 624 | { 625 | double min = Double.NaN, max = Double.NaN, multipleOf = Double.NaN; 626 | boolean exclusiveMin = false, exclusiveMax = false; 627 | public Json apply(Json param) 628 | { 629 | Json errors = null; 630 | if (!param.isNumber()) return errors; 631 | double value = param.asDouble(); 632 | if (!Double.isNaN(min) && (value < min || exclusiveMin && value == min)) 633 | errors = maybeError(errors,Json.make("Number " + param + " is below allowed minimum " + min)); 634 | if (!Double.isNaN(max) && (value > max || exclusiveMax && value == max)) 635 | errors = maybeError(errors,Json.make("Number " + param + " is above allowed maximum " + max)); 636 | if (!Double.isNaN(multipleOf) && (value / multipleOf) % 1 != 0) 637 | errors = maybeError(errors,Json.make("Number " + param + " is not a multiple of " + multipleOf)); 638 | return errors; 639 | } 640 | } 641 | 642 | class CheckArray implements Instruction 643 | { 644 | int min = 0, max = Integer.MAX_VALUE; 645 | Boolean uniqueitems = null; 646 | Instruction additionalSchema = any; 647 | Instruction schema; 648 | ArrayList schemas; 649 | 650 | public Json apply(Json param) 651 | { 652 | Json errors = null; 653 | if (!param.isArray()) return errors; 654 | if (schema == null && schemas == null && additionalSchema == null) // no schema specified 655 | return errors; 656 | int size = param.asJsonList().size(); 657 | for (int i = 0; i < size; i++) 658 | { 659 | Instruction S = schema != null ? schema 660 | : (schemas != null && i < schemas.size()) ? schemas.get(i) : additionalSchema; 661 | if (S == null) 662 | errors = maybeError(errors,Json.make("Additional items are not permitted: " + 663 | param.at(i) + " in " + param.toString(maxchars))); 664 | else 665 | errors = maybeError(errors, S.apply(param.at(i))); 666 | if (uniqueitems != null && uniqueitems && param.asJsonList().lastIndexOf(param.at(i)) > i) 667 | errors = maybeError(errors,Json.make("Element " + param.at(i) + " is duplicate in array.")); 668 | } 669 | if (size < min || size > max) 670 | errors = maybeError(errors,Json.make("Array " + param.toString(maxchars) + 671 | " has number of elements outside of the permitted range [" + min + "," + max + "].")); 672 | return errors; 673 | } 674 | } 675 | 676 | class CheckPropertyPresent implements Instruction 677 | { 678 | String propname; 679 | public CheckPropertyPresent(String propname) { this.propname = propname; } 680 | public Json apply(Json param) 681 | { 682 | if (!param.isObject()) return null; 683 | if (param.has(propname)) return null; 684 | else return Json.array().add(Json.make("Required property " + propname + 685 | " missing from object " + param.toString(maxchars))); 686 | } 687 | } 688 | 689 | class CheckObject implements Instruction 690 | { 691 | int min = 0, max = Integer.MAX_VALUE; 692 | HashSet checked = new HashSet(); 693 | Instruction additionalSchema = any; 694 | ArrayList props = new ArrayList(); 695 | ArrayList patternProps = new ArrayList(); 696 | 697 | // Object validation 698 | class CheckProperty implements Instruction 699 | { 700 | String name; 701 | Instruction schema; 702 | public CheckProperty(String name, Instruction schema) 703 | { this.name = name; this.schema = schema; } 704 | public Json apply(Json param) 705 | { 706 | Json value = param.at(name); 707 | if (value == null) 708 | return null; 709 | else 710 | { 711 | checked.add(name); 712 | return schema.apply(param.at(name)); 713 | } 714 | } 715 | } 716 | 717 | class CheckPatternProperty implements Instruction 718 | { 719 | Pattern pattern; 720 | Instruction schema; 721 | public CheckPatternProperty(String pattern, Instruction schema) 722 | { this.pattern = Pattern.compile(pattern); this.schema = schema; } 723 | public Json apply(Json param) 724 | { 725 | Json errors = null; 726 | for (Map.Entry e : param.asJsonMap().entrySet()) 727 | if (pattern.matcher(e.getKey()).find()) 728 | { 729 | errors = maybeError(errors, schema.apply(e.getValue())); 730 | checked.add(e.getKey()); 731 | } 732 | return errors; 733 | } 734 | } 735 | 736 | public Json apply(Json param) 737 | { 738 | Json errors = null; 739 | if (!param.isObject()) return errors; 740 | checked.clear(); 741 | for (Instruction I : props) 742 | errors = maybeError(errors, I.apply(param)); 743 | for (Instruction I : patternProps) 744 | errors = maybeError(errors, I.apply(param)); 745 | if (additionalSchema != any) for (Map.Entry e : param.asJsonMap().entrySet()) 746 | if (!checked.contains(e.getKey())) 747 | errors = maybeError(errors, additionalSchema == null ? 748 | Json.make("Extra property '" + e.getKey() + 749 | "', schema doesn't allow any properties not explicitly defined:" + 750 | param.toString(maxchars)) 751 | : additionalSchema.apply(e.getValue())); 752 | if (param.asJsonMap().size() < min) 753 | errors = maybeError(errors, Json.make("Object " + param.toString(maxchars) + 754 | " has fewer than the permitted " + min + " number of properties.")); 755 | if (param.asJsonMap().size() > max) 756 | errors = maybeError(errors, Json.make("Object " + param.toString(maxchars) + 757 | " has more than the permitted " + min + " number of properties.")); 758 | return errors; 759 | } 760 | } 761 | 762 | class Sequence implements Instruction 763 | { 764 | ArrayList seq = new ArrayList(); 765 | public Json apply(Json param) 766 | { 767 | Json errors = null; 768 | for (Instruction I : seq) 769 | errors = maybeError(errors, I.apply(param)); 770 | return errors; 771 | } 772 | public Sequence add(Instruction I) { seq.add(I); return this; } 773 | } 774 | 775 | class CheckType implements Instruction 776 | { 777 | Json types; 778 | public CheckType(Json types) { this.types = types; } 779 | public Json apply(Json param) 780 | { 781 | String ptype = param.isString() ? "string" : 782 | param.isObject() ? "object" : 783 | param.isArray() ? "array" : 784 | param.isNumber() ? "number" : 785 | param.isNull() ? "null" : "boolean"; 786 | for (Json type : types.asJsonList()) 787 | if (type.asString().equals(ptype)) 788 | return null; 789 | else if (type.asString().equals("integer") && 790 | param.isNumber() && 791 | param.asDouble() % 1 == 0) 792 | return null; 793 | return Json.array().add(Json.make("Type mistmatch for " + param.toString(maxchars) + 794 | ", allowed types: " + types)); 795 | } 796 | } 797 | 798 | class CheckEnum implements Instruction 799 | { 800 | Json theenum; 801 | public CheckEnum(Json theenum) { this.theenum = theenum; } 802 | public Json apply(Json param) 803 | { 804 | for (Json option : theenum.asJsonList()) 805 | if (param.equals(option)) 806 | return null; 807 | return Json.array().add("Element " + param.toString(maxchars) + 808 | " doesn't match any of enumerated possibilities " + theenum); 809 | } 810 | } 811 | 812 | class CheckAny implements Instruction 813 | { 814 | ArrayList alternates = new ArrayList(); 815 | Json schema; 816 | public Json apply(Json param) 817 | { 818 | for (Instruction I : alternates) 819 | if (I.apply(param) == null) 820 | return null; 821 | return Json.array().add("Element " + param.toString(maxchars) + 822 | " must conform to at least one of available sub-schemas " + 823 | schema.toString(maxchars)); 824 | } 825 | } 826 | 827 | class CheckOne implements Instruction 828 | { 829 | ArrayList alternates = new ArrayList(); 830 | Json schema; 831 | public Json apply(Json param) 832 | { 833 | int matches = 0; 834 | for (Instruction I : alternates) 835 | if (I.apply(param) == null) 836 | matches++; 837 | if (matches != 1) 838 | return Json.array().add("Element " + param.toString(maxchars) + 839 | " must conform to exactly one of available sub-schemas, but not more " + 840 | schema.toString(maxchars)); 841 | else 842 | return null; 843 | } 844 | } 845 | 846 | class CheckNot implements Instruction 847 | { 848 | Instruction I; 849 | Json schema; 850 | public CheckNot(Instruction I, Json schema) { this.I = I; this.schema = schema; } 851 | public Json apply(Json param) 852 | { 853 | if (I.apply(param) != null) 854 | return null; 855 | else 856 | return Json.array().add("Element " + param.toString(maxchars) + 857 | " must NOT conform to the schema " + schema.toString(maxchars)); 858 | } 859 | } 860 | 861 | class CheckSchemaDependency implements Instruction 862 | { 863 | Instruction schema; 864 | String property; 865 | public CheckSchemaDependency(String property, Instruction schema) { this.property = property; this.schema = schema; } 866 | public Json apply(Json param) 867 | { 868 | if (!param.isObject()) return null; 869 | else if (!param.has(property)) return null; 870 | else return (schema.apply(param)); 871 | } 872 | } 873 | 874 | class CheckPropertyDependency implements Instruction 875 | { 876 | Json required; 877 | String property; 878 | public CheckPropertyDependency(String property, Json required) { this.property = property; this.required = required; } 879 | public Json apply(Json param) 880 | { 881 | if (!param.isObject()) return null; 882 | if (!param.has(property)) return null; 883 | else 884 | { 885 | Json errors = null; 886 | for (Json p : required.asJsonList()) 887 | if (!param.has(p.asString())) 888 | errors = maybeError(errors, Json.make("Conditionally required property " + p + 889 | " missing from object " + param.toString(maxchars))); 890 | return errors; 891 | } 892 | } 893 | } 894 | 895 | Instruction compile(Json S, Map compiled) 896 | { 897 | Instruction result = compiled.get(S); 898 | if (result != null) 899 | return result; 900 | Sequence seq = new Sequence(); 901 | compiled.put(S, seq); 902 | if (S.has("type") && !S.is("type", "any")) 903 | seq.add(new CheckType(S.at("type").isString() ? 904 | Json.array().add(S.at("type")) : S.at("type"))); 905 | if (S.has("enum")) 906 | seq.add(new CheckEnum(S.at("enum"))); 907 | if (S.has("allOf")) 908 | { 909 | Sequence sub = new Sequence(); 910 | for (Json x : S.at("allOf").asJsonList()) 911 | sub.add(compile(x, compiled)); 912 | seq.add(sub); 913 | } 914 | if (S.has("anyOf")) 915 | { 916 | CheckAny any = new CheckAny(); 917 | any.schema = S.at("anyOf"); 918 | for (Json x : any.schema.asJsonList()) 919 | any.alternates.add(compile(x, compiled)); 920 | seq.add(any); 921 | } 922 | if (S.has("oneOf")) 923 | { 924 | CheckOne any = new CheckOne(); 925 | any.schema = S.at("oneOf"); 926 | for (Json x : any.schema.asJsonList()) 927 | any.alternates.add(compile(x, compiled)); 928 | seq.add(any); 929 | } 930 | if (S.has("not")) 931 | seq.add(new CheckNot(compile(S.at("not"), compiled), S.at("not"))); 932 | 933 | if (S.has("required")) 934 | for (Json p : S.at("required").asJsonList()) 935 | seq.add(new CheckPropertyPresent(p.asString())); 936 | 937 | CheckObject objectCheck = new CheckObject(); 938 | if (S.has("properties")) 939 | for (Map.Entry p : S.at("properties").asJsonMap().entrySet()) 940 | objectCheck.props.add(objectCheck.new CheckProperty( 941 | p.getKey(), compile(p.getValue(), compiled))); 942 | if (S.has("patternProperties")) 943 | for (Map.Entry p : S.at("patternProperties").asJsonMap().entrySet()) 944 | objectCheck.patternProps.add(objectCheck.new CheckPatternProperty(p.getKey(), 945 | compile(p.getValue(), compiled))); 946 | if (S.has("additionalProperties")) 947 | { 948 | if (S.at("additionalProperties").isObject()) 949 | objectCheck.additionalSchema = compile(S.at("additionalProperties"), compiled); 950 | else if (!S.at("additionalProperties").asBoolean()) 951 | objectCheck.additionalSchema = null; // means no additional properties allowed 952 | } 953 | if (S.has("minProperties")) 954 | objectCheck.min = S.at("minProperties").asInteger(); 955 | if (S.has("maxProperties")) 956 | objectCheck.max = S.at("maxProperties").asInteger(); 957 | 958 | if (!objectCheck.props.isEmpty() || !objectCheck.patternProps.isEmpty() || 959 | objectCheck.additionalSchema != any || 960 | objectCheck.min > 0 || objectCheck.max < Integer.MAX_VALUE) 961 | seq.add(objectCheck); 962 | 963 | CheckArray arrayCheck = new CheckArray(); 964 | if (S.has("items")) 965 | if (S.at("items").isObject()) 966 | arrayCheck.schema = compile(S.at("items"), compiled); 967 | else 968 | { 969 | arrayCheck.schemas = new ArrayList(); 970 | for (Json s : S.at("items").asJsonList()) 971 | arrayCheck.schemas.add(compile(s, compiled)); 972 | } 973 | if (S.has("additionalItems")) 974 | if (S.at("additionalItems").isObject()) 975 | arrayCheck.additionalSchema = compile(S.at("additionalItems"), compiled); 976 | else if (!S.at("additionalItems").asBoolean()) 977 | arrayCheck.additionalSchema = null; 978 | if (S.has("uniqueItems")) 979 | arrayCheck.uniqueitems = S.at("uniqueItems").asBoolean(); 980 | if (S.has("minItems")) 981 | arrayCheck.min = S.at("minItems").asInteger(); 982 | if (S.has("maxItems")) 983 | arrayCheck.max = S.at("maxItems").asInteger(); 984 | if (arrayCheck.schema != null || arrayCheck.schemas != null || 985 | arrayCheck.additionalSchema != any || 986 | arrayCheck.uniqueitems != null || 987 | arrayCheck.max < Integer.MAX_VALUE || arrayCheck.min > 0) 988 | seq.add(arrayCheck); 989 | 990 | CheckNumber numberCheck = new CheckNumber(); 991 | if (S.has("minimum")) 992 | numberCheck.min = S.at("minimum").asDouble(); 993 | if (S.has("maximum")) 994 | numberCheck.max = S.at("maximum").asDouble(); 995 | if (S.has("multipleOf")) 996 | numberCheck.multipleOf = S.at("multipleOf").asDouble(); 997 | if (S.has("exclusiveMinimum")) 998 | numberCheck.exclusiveMin = S.at("exclusiveMinimum").asBoolean(); 999 | if (S.has("exclusiveMaximum")) 1000 | numberCheck.exclusiveMax = S.at("exclusiveMaximum").asBoolean(); 1001 | if (!Double.isNaN(numberCheck.min) || !Double.isNaN(numberCheck.max) || !Double.isNaN(numberCheck.multipleOf)) 1002 | seq.add(numberCheck); 1003 | 1004 | CheckString stringCheck = new CheckString(); 1005 | if (S.has("minLength")) 1006 | stringCheck.min = S.at("minLength").asInteger(); 1007 | if (S.has("maxLength")) 1008 | stringCheck.max = S.at("maxLength").asInteger(); 1009 | if (S.has("pattern")) 1010 | stringCheck.pattern = Pattern.compile(S.at("pattern").asString()); 1011 | if (stringCheck.min > 0 || stringCheck.max < Integer.MAX_VALUE || stringCheck.pattern != null) 1012 | seq.add(stringCheck); 1013 | 1014 | if (S.has("dependencies")) 1015 | for (Map.Entry e : S.at("dependencies").asJsonMap().entrySet()) 1016 | if (e.getValue().isObject()) 1017 | seq.add(new CheckSchemaDependency(e.getKey(), compile(e.getValue(), compiled))); 1018 | else if (e.getValue().isArray()) 1019 | seq.add(new CheckPropertyDependency(e.getKey(), e.getValue())); 1020 | else 1021 | seq.add(new CheckPropertyDependency(e.getKey(), Json.array(e.getValue()))); 1022 | result = seq.seq.size() == 1 ? seq.seq.get(0) : seq; 1023 | compiled.put(S, result); 1024 | return result; 1025 | } 1026 | 1027 | int maxchars = 50; 1028 | URI uri; 1029 | Json theschema; 1030 | Instruction start; 1031 | 1032 | DefaultSchema(URI uri, Json theschema, Function relativeReferenceResolver) 1033 | { 1034 | try 1035 | { 1036 | this.uri = uri == null ? new URI("") : uri; 1037 | if (relativeReferenceResolver == null) 1038 | relativeReferenceResolver = docuri -> { 1039 | try { return Json.read(fetchContent(docuri.toURL())); } 1040 | catch(Exception ex) { throw new RuntimeException(ex); } 1041 | }; 1042 | this.theschema = expandReferences(theschema, 1043 | theschema, 1044 | this.uri, 1045 | new HashMap(), 1046 | new IdentityHashMap(), 1047 | relativeReferenceResolver); 1048 | } 1049 | catch (Exception ex) { throw new RuntimeException(ex); } 1050 | this.start = compile(this.theschema, new IdentityHashMap()); 1051 | } 1052 | 1053 | public Json validate(Json document) 1054 | { 1055 | Json result = Json.object("ok", true); 1056 | Json errors = start.apply(document); 1057 | return errors == null ? result : result.set("errors", errors).set("ok", false); 1058 | } 1059 | 1060 | public Json generate(Json options) 1061 | { 1062 | // TODO... 1063 | return Json.nil(); 1064 | } 1065 | } 1066 | 1067 | public static Schema schema(Json S) 1068 | { 1069 | return new DefaultSchema(null, S, null); 1070 | } 1071 | 1072 | public static Schema schema(URI uri) 1073 | { 1074 | return schema(uri, null); 1075 | } 1076 | 1077 | public static Schema schema(URI uri, Function relativeReferenceResolver) 1078 | { 1079 | try { return new DefaultSchema(uri, Json.read(Json.fetchContent(uri.toURL())), relativeReferenceResolver); } 1080 | catch (Exception ex) { throw new RuntimeException(ex); } 1081 | } 1082 | 1083 | public static Schema schema(Json S, URI uri) 1084 | { 1085 | return new DefaultSchema(uri, S, null); 1086 | } 1087 | 1088 | public static class DefaultFactory implements Factory 1089 | { 1090 | public Json nil() { return Json.topnull; } 1091 | public Json bool(boolean x) { return new BooleanJson(x ? Boolean.TRUE : Boolean.FALSE, null); } 1092 | public Json string(String x) { return new StringJson(x, null); } 1093 | public Json number(Number x) { return new NumberJson(x, null); } 1094 | public Json array() { return new ArrayJson(); } 1095 | public Json object() { return new ObjectJson(); } 1096 | public Json make(Object anything) 1097 | { 1098 | if (anything == null) 1099 | return topnull; 1100 | else if (anything instanceof Json) 1101 | return (Json)anything; 1102 | else if (anything instanceof String) 1103 | return factory().string((String)anything); 1104 | else if (anything instanceof Collection) 1105 | { 1106 | Json L = array(); 1107 | for (Object x : (Collection)anything) 1108 | L.add(factory().make(x)); 1109 | return L; 1110 | } 1111 | else if (anything instanceof Map) 1112 | { 1113 | Json O = object(); 1114 | for (Map.Entry x : ((Map)anything).entrySet()) 1115 | O.set(x.getKey().toString(), factory().make(x.getValue())); 1116 | return O; 1117 | } 1118 | else if (anything instanceof Boolean) 1119 | return factory().bool((Boolean)anything); 1120 | else if (anything instanceof Number) 1121 | return factory().number((Number)anything); 1122 | else if (anything.getClass().isArray()) 1123 | { 1124 | Class comp = anything.getClass().getComponentType(); 1125 | if (!comp.isPrimitive()) 1126 | return Json.array((Object[])anything); 1127 | Json A = array(); 1128 | if (boolean.class == comp) 1129 | for (boolean b : (boolean[])anything) A.add(b); 1130 | else if (byte.class == comp) 1131 | for (byte b : (byte[])anything) A.add(b); 1132 | else if (char.class == comp) 1133 | for (char b : (char[])anything) A.add(b); 1134 | else if (short.class == comp) 1135 | for (short b : (short[])anything) A.add(b); 1136 | else if (int.class == comp) 1137 | for (int b : (int[])anything) A.add(b); 1138 | else if (long.class == comp) 1139 | for (long b : (long[])anything) A.add(b); 1140 | else if (float.class == comp) 1141 | for (float b : (float[])anything) A.add(b); 1142 | else if (double.class == comp) 1143 | for (double b : (double[])anything) A.add(b); 1144 | return A; 1145 | } 1146 | else 1147 | throw new IllegalArgumentException("Don't know how to convert to Json : " + anything); 1148 | } 1149 | } 1150 | 1151 | public static final Factory defaultFactory = new DefaultFactory(); 1152 | 1153 | private static Factory globalFactory = defaultFactory; 1154 | 1155 | // TODO: maybe use initialValue thread-local method to attach global factory by default here... 1156 | private static ThreadLocal threadFactory = new ThreadLocal(); 1157 | 1158 | /** 1159 | *

Return the {@link Factory} currently in effect. This is the factory that the {@link #make(Object)} method 1160 | * will dispatch on upon determining the type of its argument. If you already know the type 1161 | * of element to construct, you can avoid the type introspection implicit to the make method 1162 | * and call the factory directly. This will result in an optimization.

1163 | * 1164 | * @return the factory 1165 | */ 1166 | public static Factory factory() 1167 | { 1168 | Factory f = threadFactory.get(); 1169 | return f != null ? f : globalFactory; 1170 | } 1171 | 1172 | /** 1173 | *

1174 | * Specify a global Json {@link Factory} to be used by all threads that don't have a 1175 | * specific thread-local factory attached to them. 1176 | *

1177 | * 1178 | * @param factory The new global factory 1179 | */ 1180 | public static void setGlobalFactory(Factory factory) { globalFactory = factory; } 1181 | 1182 | /** 1183 | *

1184 | * Attach a thread-local Json {@link Factory} to be used specifically by this thread. Thread-local 1185 | * Json factories are the only means to have different {@link Factory} implementations used simultaneously 1186 | * in the same application (well, more accurately, the same ClassLoader). 1187 | *

1188 | * 1189 | * @param factory the new thread local factory 1190 | */ 1191 | public static void attachFactory(Factory factory) { threadFactory.set(factory); } 1192 | 1193 | /** 1194 | *

1195 | * Clear the thread-local factory previously attached to this thread via the 1196 | * {@link #attachFactory(Factory)} method. The global factory takes effect after 1197 | * a call to this method. 1198 | *

1199 | */ 1200 | public static void detachFactory() { threadFactory.remove(); } 1201 | 1202 | /** 1203 | *

1204 | * Parse a JSON entity from its string representation. 1205 | *

1206 | * 1207 | * @param jsonAsString A valid JSON representation as per the json.org 1208 | * grammar. Cannot be null. 1209 | * @return The JSON entity parsed: an object, array, string, number or boolean, or null. Note that 1210 | * this method will never return the actual Java null. 1211 | */ 1212 | public static Json read(String jsonAsString) { return (Json)new Reader().read(jsonAsString); } 1213 | 1214 | /** 1215 | *

1216 | * Parse a JSON entity from a URL. 1217 | *

1218 | * 1219 | * @param location A valid URL where to load a JSON document from. Cannot be null. 1220 | * @return The JSON entity parsed: an object, array, string, number or boolean, or null. Note that 1221 | * this method will never return the actual Java null. 1222 | */ 1223 | public static Json read(URL location) { return (Json)new Reader().read(fetchContent(location)); } 1224 | 1225 | /** 1226 | *

1227 | * Parse a JSON entity from a {@link CharacterIterator}. 1228 | *

1229 | * @param it A character iterator. 1230 | * @return the parsed JSON element 1231 | * @see #read(String) 1232 | */ 1233 | public static Json read(CharacterIterator it) { return (Json)new Reader().read(it); } 1234 | /** 1235 | * @return the null Json instance. 1236 | */ 1237 | public static Json nil() { return factory().nil(); } 1238 | /** 1239 | * @return a newly constructed, empty JSON object. 1240 | */ 1241 | public static Json object() { return factory().object(); } 1242 | /** 1243 | *

Return a new JSON object initialized from the passed list of 1244 | * name/value pairs. The number of arguments must 1245 | * be even. Each argument at an even position is taken to be a name 1246 | * for the following value. The name arguments are normally of type 1247 | * Java String, but they can be of any other type having an appropriate 1248 | * toString method. Each value is first converted 1249 | * to a Json instance using the {@link #make(Object)} method. 1250 | *

1251 | * @param args A sequence of name value pairs. 1252 | * @return the new JSON object. 1253 | */ 1254 | public static Json object(Object...args) 1255 | { 1256 | Json j = object(); 1257 | if (args.length % 2 != 0) 1258 | throw new IllegalArgumentException("An even number of arguments is expected."); 1259 | for (int i = 0; i < args.length; i++) 1260 | j.set(args[i].toString(), factory().make(args[++i])); 1261 | return j; 1262 | } 1263 | 1264 | /** 1265 | * @return a new constructed, empty JSON array. 1266 | */ 1267 | public static Json array() { return factory().array(); } 1268 | 1269 | /** 1270 | *

Return a new JSON array filled up with the list of arguments.

1271 | * 1272 | * @param args The initial content of the array. 1273 | * @return the new JSON array 1274 | */ 1275 | public static Json array(Object...args) 1276 | { 1277 | Json A = array(); 1278 | for (Object x : args) 1279 | A.add(factory().make(x)); 1280 | return A; 1281 | } 1282 | 1283 | /** 1284 | *

1285 | * Convert an arbitrary Java instance to a {@link Json} instance. 1286 | *

1287 | * 1288 | *

1289 | * Maps, Collections and arrays are recursively copied where each of 1290 | * their elements concerted into Json instances as well. The keys 1291 | * of a {@link Map} parameter are normally strings, but anything with a meaningful 1292 | * toString implementation will work as well. 1293 | *

1294 | * 1295 | * @param anything Any Java object that the current JSON factory in effect is capable of handling. 1296 | * @return The Json. This method will never return null. It will 1297 | * throw an {@link IllegalArgumentException} if it doesn't know how to convert the argument 1298 | * to a Json instance. 1299 | * @throws IllegalArgumentException when the concrete type of the parameter is 1300 | * unknown. 1301 | */ 1302 | public static Json make(Object anything) 1303 | { 1304 | return factory().make(anything); 1305 | } 1306 | 1307 | // end of static utility method section 1308 | 1309 | Json enclosing = null; 1310 | 1311 | protected Json() { } 1312 | protected Json(Json enclosing) { this.enclosing = enclosing; } 1313 | 1314 | /** 1315 | *

Return a string representation of this that does 1316 | * not exceed a certain maximum length. This is useful in constructing 1317 | * error messages or any other place where only a "preview" of the 1318 | * JSON element should be displayed. Some JSON structures can get 1319 | * very large and this method will help avoid string serializing 1320 | * the whole of them.

1321 | * @param maxCharacters The maximum number of characters for 1322 | * the string representation. 1323 | * @return The string representation of this object. 1324 | */ 1325 | public String toString(int maxCharacters) { return toString(); } 1326 | 1327 | /** 1328 | *

Explicitly set the parent of this element. The parent is presumably an array 1329 | * or an object. Normally, there's no need to call this method as the parent is 1330 | * automatically set by the framework. You may need to call it however, if you implement 1331 | * your own {@link Factory} with your own implementations of the Json types. 1332 | *

1333 | * 1334 | * @param enclosing The parent element. 1335 | */ 1336 | public void attachTo(Json enclosing) { this.enclosing = enclosing; } 1337 | 1338 | /** 1339 | * @return the Json entity, if any, enclosing this 1340 | * Json. The returned value can be null or 1341 | * a Json object or list, but not one of the primitive types. 1342 | */ 1343 | public final Json up() { return enclosing; } 1344 | 1345 | /** 1346 | * @return a clone (a duplicate) of this Json entity. Note that cloning 1347 | * is deep if array and objects. Primitives are also cloned, even though their values are immutable 1348 | * because the new enclosing entity (the result of the {@link #up()} method) may be different. 1349 | * since they are immutable. 1350 | */ 1351 | public Json dup() { return this; } 1352 | 1353 | /** 1354 | *

Return the Json element at the specified index of this 1355 | * Json array. This method applies only to Json arrays. 1356 | *

1357 | * 1358 | * @param index The index of the desired element. 1359 | * @return The JSON element at the specified index in this array. 1360 | */ 1361 | public Json at(int index) { throw new UnsupportedOperationException(); } 1362 | 1363 | /** 1364 | *

1365 | * Return the specified property of a Json object or null 1366 | * if there's no such property. This method applies only to Json objects. 1367 | *

1368 | * @param The property name. 1369 | * @return The JSON element that is the value of that property. 1370 | */ 1371 | public Json at(String property) { throw new UnsupportedOperationException(); } 1372 | 1373 | /** 1374 | *

1375 | * Return the specified property of a Json object if it exists. 1376 | * If it doesn't, then create a new property with value the def 1377 | * parameter and return that parameter. 1378 | *

1379 | * 1380 | * @param property The property to return. 1381 | * @param def The default value to set and return in case the property doesn't exist. 1382 | */ 1383 | public final Json at(String property, Json def) 1384 | { 1385 | Json x = at(property); 1386 | if (x == null) 1387 | { 1388 | set(property, def); 1389 | return def; 1390 | } 1391 | else 1392 | return x; 1393 | } 1394 | 1395 | /** 1396 | *

1397 | * Return the specified property of a Json object if it exists. 1398 | * If it doesn't, then create a new property with value the def 1399 | * parameter and return that parameter. 1400 | *

1401 | * 1402 | * @param property The property to return. 1403 | * @param def The default value to set and return in case the property doesn't exist. 1404 | */ 1405 | public final Json at(String property, Object def) 1406 | { 1407 | return at(property, make(def)); 1408 | } 1409 | 1410 | /** 1411 | *

1412 | * Return true if this Json object has the specified property 1413 | * and false otherwise. 1414 | *

1415 | * 1416 | * @param property The name of the property. 1417 | */ 1418 | public boolean has(String property) { throw new UnsupportedOperationException(); } 1419 | 1420 | /** 1421 | *

1422 | * Return true if and only if this Json object has a property with 1423 | * the specified value. In particular, if the object has no such property false is returned. 1424 | *

1425 | * 1426 | * @param property The property name. 1427 | * @param value The value to compare with. Comparison is done via the equals method. 1428 | * If the value is not an instance of Json, it is first converted to 1429 | * such an instance. 1430 | * @return 1431 | */ 1432 | public boolean is(String property, Object value) { throw new UnsupportedOperationException(); } 1433 | 1434 | /** 1435 | *

1436 | * Return true if and only if this Json array has an element with 1437 | * the specified value at the specified index. In particular, if the array has no element at 1438 | * this index, false is returned. 1439 | *

1440 | * 1441 | * @param index The 0-based index of the element in a JSON array. 1442 | * @param value The value to compare with. Comparison is done via the equals method. 1443 | * If the value is not an instance of Json, it is first converted to 1444 | * such an instance. 1445 | * @return 1446 | */ 1447 | public boolean is(int index, Object value) { throw new UnsupportedOperationException(); } 1448 | 1449 | /** 1450 | *

1451 | * Add the specified Json element to this array. 1452 | *

1453 | * 1454 | * @return this 1455 | */ 1456 | public Json add(Json el) { throw new UnsupportedOperationException(); } 1457 | 1458 | /** 1459 | *

1460 | * Add an arbitrary Java object to this Json array. The object 1461 | * is first converted to a Json instance by calling the static 1462 | * {@link #make} method. 1463 | *

1464 | * 1465 | * @param anything Any Java object that can be converted to a Json instance. 1466 | * @return this 1467 | */ 1468 | public final Json add(Object anything) { return add(make(anything)); } 1469 | 1470 | /** 1471 | *

1472 | * Remove the specified property from a Json object and return 1473 | * that property. 1474 | *

1475 | * 1476 | * @param property The property to be removed. 1477 | * @return The property value or null if the object didn't have such 1478 | * a property to begin with. 1479 | */ 1480 | public Json atDel(String property) { throw new UnsupportedOperationException(); } 1481 | 1482 | /** 1483 | *

1484 | * Remove the element at the specified index from a Json array and return 1485 | * that element. 1486 | *

1487 | * 1488 | * @param index The index of the element to delete. 1489 | * @return The element value. 1490 | */ 1491 | public Json atDel(int index) { throw new UnsupportedOperationException(); } 1492 | 1493 | /** 1494 | *

1495 | * Delete the specified property from a Json object. 1496 | *

1497 | * 1498 | * @param property The property to be removed. 1499 | * @return this 1500 | */ 1501 | public Json delAt(String property) { throw new UnsupportedOperationException(); } 1502 | 1503 | /** 1504 | *

1505 | * Remove the element at the specified index from a Json array. 1506 | *

1507 | * 1508 | * @param index The index of the element to delete. 1509 | * @return this 1510 | */ 1511 | public Json delAt(int index) { throw new UnsupportedOperationException(); } 1512 | 1513 | /** 1514 | *

1515 | * Remove the specified element from a Json array. 1516 | *

1517 | * 1518 | * @param el The element to delete. 1519 | * @return this 1520 | */ 1521 | public Json remove(Json el) { throw new UnsupportedOperationException(); } 1522 | 1523 | /** 1524 | *

1525 | * Remove the specified Java object (converted to a Json instance) 1526 | * from a Json array. This is equivalent to 1527 | * remove({@link #make(Object)}). 1528 | *

1529 | * 1530 | * @param anything The object to delete. 1531 | * @return this 1532 | */ 1533 | public final Json remove(Object anything) { return remove(make(anything)); } 1534 | 1535 | /** 1536 | *

1537 | * Set a Json objects's property. 1538 | *

1539 | * 1540 | * @param property The property name. 1541 | * @param value The value of the property. 1542 | * @return this 1543 | */ 1544 | public Json set(String property, Json value) { throw new UnsupportedOperationException(); } 1545 | 1546 | /** 1547 | *

1548 | * Set a Json objects's property. 1549 | *

1550 | * 1551 | * @param property The property name. 1552 | * @param value The value of the property, converted to a Json representation 1553 | * with {@link #make}. 1554 | * @return this 1555 | */ 1556 | public final Json set(String property, Object value) { return set(property, make(value)); } 1557 | 1558 | /** 1559 | *

1560 | * Change the value of a JSON array element. This must be an array. 1561 | *

1562 | * @param index 0-based index of the element in the array. 1563 | * @param value the new value of the element 1564 | * @return this 1565 | */ 1566 | public Json set(int index, Object value) { throw new UnsupportedOperationException(); } 1567 | 1568 | /** 1569 | *

1570 | * Combine this object or array with the passed in object or array. The types of 1571 | * this and the object argument must match. If both are 1572 | * Json objects, all properties of the parameter are added to this. 1573 | * If both are arrays, all elements of the parameter are appended to this 1574 | *

1575 | * @param object The object or array whose properties or elements must be added to this 1576 | * Json object or array. 1577 | * @return this 1578 | */ 1579 | public Json with(Json object, Json[]options) { throw new UnsupportedOperationException(); } 1580 | 1581 | /** 1582 | * Same as {}@link #with(Json,Json...options)} with each option 1583 | * argument converted to Json first. 1584 | */ 1585 | public Json with(Json object, Object...options) 1586 | { 1587 | Json [] jopts = new Json[options.length]; 1588 | for (int i = 0; i < jopts.length; i++) 1589 | jopts[i] = make(options[i]); 1590 | return with(object, jopts); 1591 | } 1592 | 1593 | /** 1594 | * @return the underlying value of this Json entity. The actual value will 1595 | * be a Java Boolean, String, Number, Map, List or null. For complex entities (objects 1596 | * or arrays), the method will perform a deep copy and extra underlying values recursively 1597 | * for all nested elements. 1598 | */ 1599 | public Object getValue() { throw new UnsupportedOperationException(); } 1600 | 1601 | /** 1602 | * @return the boolean value of a boolean Json instance. Call 1603 | * {@link #isBoolean()} first if you're not sure this instance is indeed a 1604 | * boolean. 1605 | */ 1606 | public boolean asBoolean() { throw new UnsupportedOperationException(); } 1607 | 1608 | /** 1609 | * @return the string value of a string Json instance. Call 1610 | * {@link #isString()} first if you're not sure this instance is indeed a 1611 | * string. 1612 | */ 1613 | public String asString() { throw new UnsupportedOperationException(); } 1614 | 1615 | /** 1616 | * @return the integer value of a number Json instance. Call 1617 | * {@link #isNumber()} first if you're not sure this instance is indeed a 1618 | * number. 1619 | */ 1620 | public int asInteger() { throw new UnsupportedOperationException(); } 1621 | 1622 | /** 1623 | * @return the float value of a float Json instance. Call 1624 | * {@link #isNumber()} first if you're not sure this instance is indeed a 1625 | * number. 1626 | */ 1627 | public float asFloat() { throw new UnsupportedOperationException(); } 1628 | 1629 | /** 1630 | * @return the double value of a number Json instance. Call 1631 | * {@link #isNumber()} first if you're not sure this instance is indeed a 1632 | * number. 1633 | */ 1634 | public double asDouble() { throw new UnsupportedOperationException(); } 1635 | 1636 | /** 1637 | * @return the long value of a number Json instance. Call 1638 | * {@link #isNumber()} first if you're not sure this instance is indeed a 1639 | * number. 1640 | */ 1641 | public long asLong() { throw new UnsupportedOperationException(); } 1642 | 1643 | /** 1644 | * @return the short value of a number Json instance. Call 1645 | * {@link #isNumber()} first if you're not sure this instance is indeed a 1646 | * number. 1647 | */ 1648 | public short asShort() { throw new UnsupportedOperationException(); } 1649 | 1650 | /** 1651 | * @return the byte value of a number Json instance. Call 1652 | * {@link #isNumber()} first if you're not sure this instance is indeed a 1653 | * number. 1654 | */ 1655 | public byte asByte() { throw new UnsupportedOperationException(); } 1656 | 1657 | /** 1658 | * @return the first character of a string Json instance. Call 1659 | * {@link #isString()} first if you're not sure this instance is indeed a 1660 | * string. 1661 | */ 1662 | public char asChar() { throw new UnsupportedOperationException(); } 1663 | 1664 | /** 1665 | * @return a map of the properties of an object Json instance. The map 1666 | * is a clone of the object and can be modified safely without affecting it. Call 1667 | * {@link #isObject()} first if you're not sure this instance is indeed a 1668 | * Json object. 1669 | */ 1670 | public Map asMap() { throw new UnsupportedOperationException(); } 1671 | 1672 | /** 1673 | * @return the underlying map of properties of a Json object. The returned 1674 | * map is the actual object representation so any modifications to it are modifications 1675 | * of the Json object itself. Call 1676 | * {@link #isObject()} first if you're not sure this instance is indeed a 1677 | * Json object. 1678 | */ 1679 | public Map asJsonMap() { throw new UnsupportedOperationException(); } 1680 | 1681 | /** 1682 | * @return a list of the elements of a Json array. The list is a clone 1683 | * of the array and can be modified safely without affecting it. Call 1684 | * {@link #isArray()} first if you're not sure this instance is indeed a 1685 | * Json array. 1686 | */ 1687 | public List asList() { throw new UnsupportedOperationException(); } 1688 | 1689 | /** 1690 | * @return the underlying {@link List} representation of a Json array. 1691 | * The returned list is the actual array representation so any modifications to it 1692 | * are modifications of the Json array itself. Call 1693 | * {@link #isArray()} first if you're not sure this instance is indeed a 1694 | * Json array. 1695 | */ 1696 | public List asJsonList() { throw new UnsupportedOperationException(); } 1697 | 1698 | /** 1699 | * @return true if this is a Json null entity 1700 | * and false otherwise. 1701 | */ 1702 | public boolean isNull() { return false; } 1703 | /** 1704 | * @return true if this is a Json string entity 1705 | * and false otherwise. 1706 | */ 1707 | public boolean isString() { return false; } 1708 | /** 1709 | * @return true if this is a Json number entity 1710 | * and false otherwise. 1711 | */ 1712 | public boolean isNumber() { return false; } 1713 | /** 1714 | * @return true if this is a Json boolean entity 1715 | * and false otherwise. 1716 | */ 1717 | public boolean isBoolean() { return false; } 1718 | /** 1719 | * @return true if this is a Json array (i.e. list) entity 1720 | * and false otherwise. 1721 | */ 1722 | public boolean isArray() { return false; } 1723 | /** 1724 | * @return true if this is a Json object entity 1725 | * and false otherwise. 1726 | */ 1727 | public boolean isObject(){ return false; } 1728 | /** 1729 | * @return true if this is a Json primitive entity 1730 | * (one of string, number or boolean) and false otherwise. 1731 | * 1732 | */ 1733 | public boolean isPrimitive() { return isString() || isNumber() || isBoolean(); } 1734 | 1735 | /** 1736 | *

1737 | * Json-pad this object as an argument to a callback function. 1738 | *

1739 | * 1740 | * @param callback The name of the callback function. Can be null or empty, 1741 | * in which case no padding is done. 1742 | * @return The jsonpadded, stringified version of this object if the callback 1743 | * is not null or empty, or just the stringified version of the object. 1744 | */ 1745 | public String pad(String callback) 1746 | { 1747 | return (callback != null && callback.length() > 0) 1748 | ? callback + "(" + toString() + ");" 1749 | : toString(); 1750 | } 1751 | 1752 | //------------------------------------------------------------------------- 1753 | // END OF PUBLIC INTERFACE 1754 | //------------------------------------------------------------------------- 1755 | 1756 | /** 1757 | * Return an object representing the complete configuration 1758 | * of a merge. The properties of the object represent paths 1759 | * of the JSON structure being merged and the values represent 1760 | * the set of options that apply to each path. 1761 | * @param options the configuration options 1762 | * @return the configuration object 1763 | */ 1764 | protected Json collectWithOptions(Json...options) 1765 | { 1766 | Json result = object(); 1767 | for (Json opt : options) 1768 | { 1769 | if (opt.isString()) 1770 | result.at("", object()).set(opt.asString(), true); 1771 | else 1772 | { 1773 | Json forPaths = opt.at("for", array("")); 1774 | if (!forPaths.isArray()) 1775 | forPaths = array(forPaths); 1776 | for (Json path : forPaths.asJsonList()) 1777 | { 1778 | Json at_path = result.at(path.asString(), object()); 1779 | at_path.set("merge", opt.is("merge", true)); 1780 | at_path.set("dup", opt.is("dup", true)); 1781 | at_path.set("sort", opt.is("sort", true)); 1782 | at_path.set("compareBy", opt.at("compareBy", nil())); 1783 | } 1784 | } 1785 | } 1786 | return result; 1787 | } 1788 | 1789 | static class NullJson extends Json 1790 | { 1791 | private static final long serialVersionUID = 1L; 1792 | 1793 | NullJson() {} 1794 | NullJson(Json e) {super(e);} 1795 | 1796 | public Object getValue() { return null; } 1797 | public Json dup() { return new NullJson(); } 1798 | public boolean isNull() { return true; } 1799 | public String toString() { return "null"; } 1800 | public List asList() { return (List)Collections.singletonList(null); } 1801 | 1802 | public int hashCode() { return 0; } 1803 | public boolean equals(Object x) 1804 | { 1805 | return x instanceof NullJson; 1806 | } 1807 | } 1808 | 1809 | static NullJson topnull = new NullJson(); 1810 | 1811 | static class BooleanJson extends Json 1812 | { 1813 | private static final long serialVersionUID = 1L; 1814 | 1815 | boolean val; 1816 | BooleanJson() {} 1817 | BooleanJson(Json e) {super(e);} 1818 | BooleanJson(Boolean val, Json e) { super(e); this.val = val; } 1819 | 1820 | public Object getValue() { return val; } 1821 | public Json dup() { return new BooleanJson(val, null); } 1822 | public boolean asBoolean() { return val; } 1823 | public boolean isBoolean() { return true; } 1824 | public String toString() { return val ? "true" : "false"; } 1825 | 1826 | @SuppressWarnings("unchecked") 1827 | public List asList() { return (List)(List)Collections.singletonList(val); } 1828 | public int hashCode() { return val ? 1 : 0; } 1829 | public boolean equals(Object x) 1830 | { 1831 | return x instanceof BooleanJson && ((BooleanJson)x).val == val; 1832 | } 1833 | } 1834 | 1835 | static class StringJson extends Json 1836 | { 1837 | private static final long serialVersionUID = 1L; 1838 | 1839 | String val; 1840 | 1841 | StringJson() {} 1842 | StringJson(Json e) {super(e);} 1843 | StringJson(String val, Json e) { super(e); this.val = val; } 1844 | 1845 | public Json dup() { return new StringJson(val, null); } 1846 | public boolean isString() { return true; } 1847 | public Object getValue() { return val; } 1848 | public String asString() { return val; } 1849 | public int asInteger() { return Integer.parseInt(val); } 1850 | public float asFloat() { return Float.parseFloat(val); } 1851 | public double asDouble() { return Double.parseDouble(val); } 1852 | public long asLong() { return Long.parseLong(val); } 1853 | public short asShort() { return Short.parseShort(val); } 1854 | public byte asByte() { return Byte.parseByte(val); } 1855 | public char asChar() { return val.charAt(0); } 1856 | @SuppressWarnings("unchecked") 1857 | public List asList() { return (List)(List)Collections.singletonList(val); } 1858 | 1859 | public String toString() 1860 | { 1861 | return '"' + escaper.escapeJsonString(val) + '"'; 1862 | } 1863 | public String toString(int maxCharacters) 1864 | { 1865 | if (val.length() <= maxCharacters) 1866 | return toString(); 1867 | else 1868 | return '"' + escaper.escapeJsonString(val.subSequence(0, maxCharacters)) + "...\""; 1869 | } 1870 | 1871 | public int hashCode() { return val.hashCode(); } 1872 | public boolean equals(Object x) 1873 | { 1874 | return x instanceof StringJson && ((StringJson)x).val.equals(val); 1875 | } 1876 | } 1877 | 1878 | static class NumberJson extends Json 1879 | { 1880 | private static final long serialVersionUID = 1L; 1881 | 1882 | Number val; 1883 | 1884 | NumberJson() {} 1885 | NumberJson(Json e) {super(e);} 1886 | NumberJson(Number val, Json e) { super(e); this.val = val; } 1887 | 1888 | public Json dup() { return new NumberJson(val, null); } 1889 | public boolean isNumber() { return true; } 1890 | public Object getValue() { return val; } 1891 | public String asString() { return val.toString(); } 1892 | public int asInteger() { return val.intValue(); } 1893 | public float asFloat() { return val.floatValue(); } 1894 | public double asDouble() { return val.doubleValue(); } 1895 | public long asLong() { return val.longValue(); } 1896 | public short asShort() { return val.shortValue(); } 1897 | public byte asByte() { return val.byteValue(); } 1898 | 1899 | @SuppressWarnings("unchecked") 1900 | public List asList() { return (List)(List)Collections.singletonList(val); } 1901 | 1902 | public String toString() { return val.toString(); } 1903 | public int hashCode() { return val.hashCode(); } 1904 | public boolean equals(Object x) 1905 | { 1906 | return x instanceof NumberJson && val.doubleValue() == ((NumberJson)x).val.doubleValue(); 1907 | } 1908 | } 1909 | 1910 | static class ArrayJson extends Json 1911 | { 1912 | private static final long serialVersionUID = 1L; 1913 | 1914 | List L = new ArrayList(); 1915 | 1916 | ArrayJson() { } 1917 | ArrayJson(Json e) { super(e); } 1918 | 1919 | 1920 | public Json dup() 1921 | { 1922 | ArrayJson j = new ArrayJson(); 1923 | for (Json e : L) 1924 | { 1925 | Json v = e.dup(); 1926 | v.enclosing = j; 1927 | j.L.add(v); 1928 | } 1929 | return j; 1930 | } 1931 | 1932 | public Json set(int index, Object value) 1933 | { 1934 | L.set(index, make(value)); 1935 | return this; 1936 | } 1937 | 1938 | public List asJsonList() { return L; } 1939 | public List asList() 1940 | { 1941 | ArrayList A = new ArrayList(); 1942 | for (Json x: L) 1943 | A.add(x.getValue()); 1944 | return A; 1945 | } 1946 | public boolean is(int index, Object value) 1947 | { 1948 | if (index < 0 || index >= L.size()) 1949 | return false; 1950 | else 1951 | return L.get(index).equals(make(value)); 1952 | } 1953 | public Object getValue() { return asList(); } 1954 | public boolean isArray() { return true; } 1955 | public Json at(int index) { return L.get(index); } 1956 | public Json add(Json el) { L.add(el); el.enclosing = this; return this; } 1957 | public Json remove(Json el) { L.remove(el); el.enclosing = null; return this; } 1958 | 1959 | boolean isEqualJson(Json left, Json right) 1960 | { 1961 | if (left == null) 1962 | return right == null; 1963 | else 1964 | return left.equals(right); 1965 | } 1966 | 1967 | boolean isEqualJson(Json left, Json right, Json fields) 1968 | { 1969 | if (fields.isNull()) 1970 | return left.equals(right); 1971 | else if (fields.isString()) 1972 | return isEqualJson(resolvePointer(fields.asString(), left), 1973 | resolvePointer(fields.asString(), right)); 1974 | else if (fields.isArray()) 1975 | { 1976 | for (Json field : fields.asJsonList()) 1977 | if (!isEqualJson(resolvePointer(field.asString(), left), 1978 | resolvePointer(field.asString(), right))) 1979 | return false; 1980 | return true; 1981 | } 1982 | else 1983 | throw new IllegalArgumentException("Compare by options should be either a property name or an array of property names: " + fields); 1984 | } 1985 | 1986 | @SuppressWarnings({ "unchecked", "rawtypes" }) 1987 | int compareJson(Json left, Json right, Json fields) 1988 | { 1989 | if (fields.isNull()) 1990 | return ((Comparable)left.getValue()).compareTo(right.getValue()); 1991 | else if (fields.isString()) 1992 | { 1993 | Json leftProperty = resolvePointer(fields.asString(), left); 1994 | Json rightProperty = resolvePointer(fields.asString(), right); 1995 | return ((Comparable)leftProperty).compareTo(rightProperty); 1996 | } 1997 | else if (fields.isArray()) 1998 | { 1999 | for (Json field : fields.asJsonList()) 2000 | { 2001 | Json leftProperty = resolvePointer(field.asString(), left); 2002 | Json rightProperty = resolvePointer(field.asString(), right); 2003 | int result = ((Comparable) leftProperty).compareTo(rightProperty); 2004 | if (result != 0) 2005 | return result; 2006 | } 2007 | return 0; 2008 | } 2009 | else 2010 | throw new IllegalArgumentException("Compare by options should be either a property name or an array of property names: " + fields); 2011 | } 2012 | 2013 | Json withOptions(Json array, Json allOptions, String path) 2014 | { 2015 | Json opts = allOptions.at(path, object()); 2016 | boolean dup = opts.is("dup", true); 2017 | Json compareBy = opts.at("compareBy", nil()); 2018 | if (opts.is("sort", true)) 2019 | { 2020 | int thisIndex = 0, thatIndex = 0; 2021 | while (thatIndex < array.asJsonList().size()) 2022 | { 2023 | Json thatElement = array.at(thatIndex); 2024 | if (thisIndex == L.size()) 2025 | { 2026 | L.add(dup ? thatElement.dup() : thatElement); 2027 | thisIndex++; 2028 | thatIndex++; 2029 | continue; 2030 | } 2031 | int compared = compareJson(at(thisIndex), thatElement, compareBy); 2032 | if (compared < 0) // this < that 2033 | thisIndex++; 2034 | else if (compared > 0) // this > that 2035 | { 2036 | L.add(thisIndex, dup ? thatElement.dup() : thatElement); 2037 | thatIndex++; 2038 | } else { // equal, ignore 2039 | thatIndex++; 2040 | } 2041 | } 2042 | } 2043 | else 2044 | { 2045 | for (Json thatElement : array.asJsonList()) 2046 | { 2047 | boolean present = false; 2048 | for (Json thisElement : L) 2049 | if (isEqualJson(thisElement, thatElement, compareBy)) 2050 | { 2051 | present = true; 2052 | break; 2053 | } 2054 | if (!present) 2055 | L.add(dup ? thatElement.dup() : thatElement); 2056 | } 2057 | } 2058 | return this; 2059 | } 2060 | 2061 | public Json with(Json object, Json...options) 2062 | { 2063 | if (object == null) return this; 2064 | if (!object.isArray()) 2065 | add(object); 2066 | else if (options.length > 0) 2067 | { 2068 | Json O = collectWithOptions(options); 2069 | return withOptions(object, O, ""); 2070 | } 2071 | else 2072 | // what about "enclosing" here? we don't have a provision where a Json 2073 | // element belongs to more than one enclosing elements... 2074 | L.addAll(((ArrayJson)object).L); 2075 | return this; 2076 | } 2077 | 2078 | public Json atDel(int index) 2079 | { 2080 | Json el = L.remove(index); 2081 | if (el != null) 2082 | el.enclosing = null; 2083 | return el; 2084 | } 2085 | 2086 | public Json delAt(int index) 2087 | { 2088 | Json el = L.remove(index); 2089 | if (el != null) 2090 | el.enclosing = null; 2091 | return this; 2092 | } 2093 | 2094 | public String toString() 2095 | { 2096 | return toString(Integer.MAX_VALUE); 2097 | } 2098 | 2099 | public String toString(int maxCharacters) 2100 | { 2101 | StringBuilder sb = new StringBuilder("["); 2102 | for (Iterator i = L.iterator(); i.hasNext(); ) 2103 | { 2104 | String s = i.next().toString(maxCharacters); 2105 | if (sb.length() + s.length() > maxCharacters) 2106 | s = s.substring(0, Math.max(0, maxCharacters - sb.length())); 2107 | else 2108 | sb.append(s); 2109 | if (i.hasNext()) 2110 | sb.append(","); 2111 | if (sb.length() >= maxCharacters) 2112 | { 2113 | sb.append("..."); 2114 | break; 2115 | } 2116 | } 2117 | sb.append("]"); 2118 | return sb.toString(); 2119 | } 2120 | 2121 | public int hashCode() { return L.hashCode(); } 2122 | public boolean equals(Object x) 2123 | { 2124 | return x instanceof ArrayJson && ((ArrayJson)x).L.equals(L); 2125 | } 2126 | } 2127 | 2128 | static class ObjectJson extends Json 2129 | { 2130 | private static final long serialVersionUID = 1L; 2131 | 2132 | Map object = new HashMap(); 2133 | 2134 | ObjectJson() { } 2135 | ObjectJson(Json e) { super(e); } 2136 | 2137 | public Json dup() 2138 | { 2139 | ObjectJson j = new ObjectJson(); 2140 | for (Map.Entry e : object.entrySet()) 2141 | { 2142 | Json v = e.getValue().dup(); 2143 | v.enclosing = j; 2144 | j.object.put(e.getKey(), v); 2145 | } 2146 | return j; 2147 | } 2148 | 2149 | public boolean has(String property) 2150 | { 2151 | return object.containsKey(property); 2152 | } 2153 | 2154 | public boolean is(String property, Object value) 2155 | { 2156 | Json p = object.get(property); 2157 | if (p == null) 2158 | return false; 2159 | else 2160 | return p.equals(make(value)); 2161 | } 2162 | 2163 | public Json at(String property) 2164 | { 2165 | return object.get(property); 2166 | } 2167 | 2168 | protected Json withOptions(Json other, Json allOptions, String path) 2169 | { 2170 | Json options = allOptions.at(path, object()); 2171 | boolean duplicate = options.is("dup", true); 2172 | if (options.is("merge", true)) 2173 | { 2174 | for (Map.Entry e : other.asJsonMap().entrySet()) 2175 | { 2176 | Json local = object.get(e.getKey()); 2177 | if (local instanceof ObjectJson) 2178 | ((ObjectJson)local).withOptions(e.getValue(), allOptions, path + "/" + e.getKey()); 2179 | else if (local instanceof ArrayJson) 2180 | ((ArrayJson)local).withOptions(e.getValue(), allOptions, path + "/" + e.getKey()); 2181 | else 2182 | set(e.getKey(), duplicate ? e.getValue().dup() : e.getValue()); 2183 | } 2184 | } 2185 | else if (duplicate) 2186 | for (Map.Entry e : other.asJsonMap().entrySet()) 2187 | set(e.getKey(), e.getValue().dup()); 2188 | else 2189 | for (Map.Entry e : other.asJsonMap().entrySet()) 2190 | set(e.getKey(), e.getValue()); 2191 | return this; 2192 | } 2193 | 2194 | public Json with(Json x, Json...options) 2195 | { 2196 | if (x == null) return this; 2197 | if (!x.isObject()) 2198 | throw new UnsupportedOperationException(); 2199 | if (options.length > 0) 2200 | { 2201 | Json O = collectWithOptions(options); 2202 | return withOptions(x, O, ""); 2203 | } 2204 | else for (Map.Entry e : x.asJsonMap().entrySet()) 2205 | set(e.getKey(), e.getValue()); 2206 | return this; 2207 | } 2208 | 2209 | public Json set(String property, Json el) 2210 | { 2211 | if (property == null) 2212 | throw new IllegalArgumentException("Null property names are not allowed, value is " + el); 2213 | el.enclosing = this; 2214 | object.put(property, el); 2215 | return this; 2216 | } 2217 | 2218 | public Json atDel(String property) 2219 | { 2220 | Json el = object.remove(property); 2221 | if (el != null) 2222 | el.enclosing = null; 2223 | return el; 2224 | } 2225 | 2226 | public Json delAt(String property) 2227 | { 2228 | Json el = object.remove(property); 2229 | if (el != null) 2230 | el.enclosing = null; 2231 | return this; 2232 | } 2233 | 2234 | public Object getValue() { return asMap(); } 2235 | public boolean isObject() { return true; } 2236 | public Map asMap() 2237 | { 2238 | HashMap m = new HashMap(); 2239 | for (Map.Entry e : object.entrySet()) 2240 | m.put(e.getKey(), e.getValue().getValue()); 2241 | return m; 2242 | } 2243 | @Override 2244 | public Map asJsonMap() { return object; } 2245 | 2246 | public String toString() 2247 | { 2248 | return toString(Integer.MAX_VALUE); 2249 | } 2250 | 2251 | public String toString(int maxCharacters) 2252 | { 2253 | StringBuilder sb = new StringBuilder("{"); 2254 | for (Iterator> i = object.entrySet().iterator(); i.hasNext(); ) 2255 | { 2256 | Map.Entry x = i.next(); 2257 | sb.append('"'); 2258 | sb.append(escaper.escapeJsonString(x.getKey())); 2259 | sb.append('"'); 2260 | sb.append(":"); 2261 | String s = x.getValue().toString(maxCharacters); 2262 | if (sb.length() + s.length() > maxCharacters) 2263 | s = s.substring(0, Math.max(0, maxCharacters - sb.length())); 2264 | sb.append(s); 2265 | if (i.hasNext()) 2266 | sb.append(","); 2267 | if (sb.length() >= maxCharacters) 2268 | { 2269 | sb.append("..."); 2270 | break; 2271 | } 2272 | } 2273 | sb.append("}"); 2274 | return sb.toString(); 2275 | } 2276 | public int hashCode() { return object.hashCode(); } 2277 | public boolean equals(Object x) 2278 | { 2279 | return x instanceof ObjectJson && ((ObjectJson)x).object.equals(object); 2280 | } 2281 | } 2282 | 2283 | // ------------------------------------------------------------------------ 2284 | // Extra utilities, taken from around the internet: 2285 | // ------------------------------------------------------------------------ 2286 | 2287 | /* 2288 | * Copyright (C) 2008 Google Inc. 2289 | * 2290 | * Licensed under the Apache License, Version 2.0 (the "License"); 2291 | * you may not use this file except in compliance with the License. 2292 | * You may obtain a copy of the License at 2293 | * 2294 | * http://www.apache.org/licenses/LICENSE-2.0 2295 | * 2296 | * Unless required by applicable law or agreed to in writing, software 2297 | * distributed under the License is distributed on an "AS IS" BASIS, 2298 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 2299 | * See the License for the specific language governing permissions and 2300 | * limitations under the License. 2301 | */ 2302 | 2303 | /** 2304 | * A utility class that is used to perform JSON escaping so that ", <, >, etc. characters are 2305 | * properly encoded in the JSON string representation before returning to the client code. 2306 | * 2307 | *

This class contains a single method to escape a passed in string value: 2308 | *

2309 | 	 *   String jsonStringValue = "beforeQuote\"afterQuote";
2310 | 	 *   String escapedValue = Escaper.escapeJsonString(jsonStringValue);
2311 | 	 * 

2312 | * 2313 | * @author Inderjeet Singh 2314 | * @author Joel Leitch 2315 | */ 2316 | static Escaper escaper = new Escaper(false); 2317 | 2318 | final static class Escaper { 2319 | 2320 | private static final char[] HEX_CHARS = { 2321 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' 2322 | }; 2323 | 2324 | private static final Set JS_ESCAPE_CHARS; 2325 | private static final Set HTML_ESCAPE_CHARS; 2326 | 2327 | static { 2328 | Set mandatoryEscapeSet = new HashSet(); 2329 | mandatoryEscapeSet.add('"'); 2330 | mandatoryEscapeSet.add('\\'); 2331 | JS_ESCAPE_CHARS = Collections.unmodifiableSet(mandatoryEscapeSet); 2332 | 2333 | Set htmlEscapeSet = new HashSet(); 2334 | htmlEscapeSet.add('<'); 2335 | htmlEscapeSet.add('>'); 2336 | htmlEscapeSet.add('&'); 2337 | htmlEscapeSet.add('='); 2338 | htmlEscapeSet.add('\''); 2339 | // htmlEscapeSet.add('/'); -- Removing slash for now since it causes some incompatibilities 2340 | HTML_ESCAPE_CHARS = Collections.unmodifiableSet(htmlEscapeSet); 2341 | } 2342 | 2343 | private final boolean escapeHtmlCharacters; 2344 | 2345 | Escaper(boolean escapeHtmlCharacters) { 2346 | this.escapeHtmlCharacters = escapeHtmlCharacters; 2347 | } 2348 | 2349 | public String escapeJsonString(CharSequence plainText) { 2350 | StringBuilder escapedString = new StringBuilder(plainText.length() + 20); 2351 | try { 2352 | escapeJsonString(plainText, escapedString); 2353 | } catch (IOException e) { 2354 | throw new RuntimeException(e); 2355 | } 2356 | return escapedString.toString(); 2357 | } 2358 | 2359 | private void escapeJsonString(CharSequence plainText, StringBuilder out) throws IOException { 2360 | int pos = 0; // Index just past the last char in plainText written to out. 2361 | int len = plainText.length(); 2362 | 2363 | for (int charCount, i = 0; i < len; i += charCount) { 2364 | int codePoint = Character.codePointAt(plainText, i); 2365 | charCount = Character.charCount(codePoint); 2366 | 2367 | if (!isControlCharacter(codePoint) && !mustEscapeCharInJsString(codePoint)) { 2368 | continue; 2369 | } 2370 | 2371 | out.append(plainText, pos, i); 2372 | pos = i + charCount; 2373 | switch (codePoint) { 2374 | case '\b': 2375 | out.append("\\b"); 2376 | break; 2377 | case '\t': 2378 | out.append("\\t"); 2379 | break; 2380 | case '\n': 2381 | out.append("\\n"); 2382 | break; 2383 | case '\f': 2384 | out.append("\\f"); 2385 | break; 2386 | case '\r': 2387 | out.append("\\r"); 2388 | break; 2389 | case '\\': 2390 | out.append("\\\\"); 2391 | break; 2392 | case '/': 2393 | out.append("\\/"); 2394 | break; 2395 | case '"': 2396 | out.append("\\\""); 2397 | break; 2398 | default: 2399 | appendHexJavaScriptRepresentation(codePoint, out); 2400 | break; 2401 | } 2402 | } 2403 | out.append(plainText, pos, len); 2404 | } 2405 | 2406 | private boolean mustEscapeCharInJsString(int codepoint) { 2407 | if (!Character.isSupplementaryCodePoint(codepoint)) { 2408 | char c = (char) codepoint; 2409 | return JS_ESCAPE_CHARS.contains(c) 2410 | || (escapeHtmlCharacters && HTML_ESCAPE_CHARS.contains(c)); 2411 | } 2412 | return false; 2413 | } 2414 | 2415 | private static boolean isControlCharacter(int codePoint) { 2416 | // JSON spec defines these code points as control characters, so they must be escaped 2417 | return codePoint < 0x20 2418 | || codePoint == 0x2028 // Line separator 2419 | || codePoint == 0x2029 // Paragraph separator 2420 | || (codePoint >= 0x7f && codePoint <= 0x9f); 2421 | } 2422 | 2423 | private static void appendHexJavaScriptRepresentation(int codePoint, Appendable out) 2424 | throws IOException { 2425 | if (Character.isSupplementaryCodePoint(codePoint)) { 2426 | // Handle supplementary unicode values which are not representable in 2427 | // javascript. We deal with these by escaping them as two 4B sequences 2428 | // so that they will round-trip properly when sent from java to javascript 2429 | // and back. 2430 | char[] surrogates = Character.toChars(codePoint); 2431 | appendHexJavaScriptRepresentation(surrogates[0], out); 2432 | appendHexJavaScriptRepresentation(surrogates[1], out); 2433 | return; 2434 | } 2435 | out.append("\\u") 2436 | .append(HEX_CHARS[(codePoint >>> 12) & 0xf]) 2437 | .append(HEX_CHARS[(codePoint >>> 8) & 0xf]) 2438 | .append(HEX_CHARS[(codePoint >>> 4) & 0xf]) 2439 | .append(HEX_CHARS[codePoint & 0xf]); 2440 | } 2441 | } 2442 | 2443 | private static class Reader 2444 | { 2445 | private static final Object OBJECT_END = new Object(); 2446 | private static final Object ARRAY_END = new Object(); 2447 | private static final Object COLON = new Object(); 2448 | private static final Object COMMA = new Object(); 2449 | public static final int FIRST = 0; 2450 | public static final int CURRENT = 1; 2451 | public static final int NEXT = 2; 2452 | 2453 | private static Map escapes = new HashMap(); 2454 | static 2455 | { 2456 | escapes.put(new Character('"'), new Character('"')); 2457 | escapes.put(new Character('\\'), new Character('\\')); 2458 | escapes.put(new Character('/'), new Character('/')); 2459 | escapes.put(new Character('b'), new Character('\b')); 2460 | escapes.put(new Character('f'), new Character('\f')); 2461 | escapes.put(new Character('n'), new Character('\n')); 2462 | escapes.put(new Character('r'), new Character('\r')); 2463 | escapes.put(new Character('t'), new Character('\t')); 2464 | } 2465 | 2466 | private CharacterIterator it; 2467 | private char c; 2468 | private Object token; 2469 | private StringBuffer buf = new StringBuffer(); 2470 | 2471 | private char next() 2472 | { 2473 | if (it.getIndex() == it.getEndIndex()) 2474 | throw new RuntimeException("Reached end of input at the " + 2475 | it.getIndex() + "th character."); 2476 | c = it.next(); 2477 | return c; 2478 | } 2479 | 2480 | private char previous() 2481 | { 2482 | c = it.previous(); 2483 | return c; 2484 | } 2485 | 2486 | private void skipWhiteSpace() 2487 | { 2488 | do 2489 | { 2490 | if (Character.isWhitespace(c)) 2491 | ; 2492 | else if (c == '/') 2493 | { 2494 | next(); 2495 | if (c == '*') 2496 | { 2497 | // skip multiline comments 2498 | while (c != CharacterIterator.DONE) 2499 | if (next() == '*' && next() == '/') 2500 | break; 2501 | if (c == CharacterIterator.DONE) 2502 | throw new RuntimeException("Unterminated comment while parsing JSON string."); 2503 | } 2504 | else if (c == '/') 2505 | while (c != '\n' && c != CharacterIterator.DONE) 2506 | next(); 2507 | else 2508 | { 2509 | previous(); 2510 | break; 2511 | } 2512 | } 2513 | else 2514 | break; 2515 | } while (next() != CharacterIterator.DONE); 2516 | } 2517 | 2518 | public Object read(CharacterIterator ci, int start) 2519 | { 2520 | it = ci; 2521 | switch (start) 2522 | { 2523 | case FIRST: 2524 | c = it.first(); 2525 | break; 2526 | case CURRENT: 2527 | c = it.current(); 2528 | break; 2529 | case NEXT: 2530 | c = it.next(); 2531 | break; 2532 | } 2533 | return read(); 2534 | } 2535 | 2536 | public Object read(CharacterIterator it) 2537 | { 2538 | return read(it, NEXT); 2539 | } 2540 | 2541 | public Object read(String string) 2542 | { 2543 | return read(new StringCharacterIterator(string), FIRST); 2544 | } 2545 | 2546 | @SuppressWarnings("unchecked") 2547 | private T read() 2548 | { 2549 | skipWhiteSpace(); 2550 | char ch = c; 2551 | next(); 2552 | switch (ch) 2553 | { 2554 | case '"': token = readString(); break; 2555 | case '[': token = readArray(); break; 2556 | case ']': token = ARRAY_END; break; 2557 | case ',': token = COMMA; break; 2558 | case '{': token = readObject(); break; 2559 | case '}': token = OBJECT_END; break; 2560 | case ':': token = COLON; break; 2561 | case 't': 2562 | if (c != 'r' || next() != 'u' || next() != 'e') 2563 | throw new RuntimeException("Invalid JSON token: expected 'true' keyword."); 2564 | next(); 2565 | token = factory().bool(Boolean.TRUE); 2566 | break; 2567 | case'f': 2568 | if (c != 'a' || next() != 'l' || next() != 's' || next() != 'e') 2569 | throw new RuntimeException("Invalid JSON token: expected 'false' keyword."); 2570 | next(); 2571 | token = factory().bool(Boolean.FALSE); 2572 | break; 2573 | case 'n': 2574 | if (c != 'u' || next() != 'l' || next() != 'l') 2575 | throw new RuntimeException("Invalid JSON token: expected 'null' keyword."); 2576 | next(); 2577 | token = nil(); 2578 | break; 2579 | default: 2580 | c = it.previous(); 2581 | if (Character.isDigit(c) || c == '-') { 2582 | token = readNumber(); 2583 | } 2584 | else throw new RuntimeException("Invalid JSON near position: " + it.getIndex()); 2585 | } 2586 | return (T)token; 2587 | } 2588 | 2589 | private String readObjectKey() 2590 | { 2591 | Object key = read(); 2592 | if (key == null) 2593 | throw new RuntimeException( 2594 | "Missing object key (don't forget to put quotes!)."); 2595 | else if (key != OBJECT_END) 2596 | return ((Json)key).asString(); 2597 | else 2598 | return key.toString(); 2599 | } 2600 | 2601 | private Json readObject() 2602 | { 2603 | Json ret = object(); 2604 | String key = readObjectKey(); 2605 | while (token != OBJECT_END) 2606 | { 2607 | read(); // should be a colon 2608 | if (token != OBJECT_END) 2609 | { 2610 | Json value = read(); 2611 | ret.set(key, value); 2612 | if (read() == COMMA) { 2613 | key = readObjectKey(); 2614 | } 2615 | } 2616 | } 2617 | return ret; 2618 | } 2619 | 2620 | private Json readArray() 2621 | { 2622 | Json ret = array(); 2623 | Object value = read(); 2624 | while (token != ARRAY_END) 2625 | { 2626 | ret.add((Json)value); 2627 | if (read() == COMMA) 2628 | value = read(); 2629 | else if (token != ARRAY_END) 2630 | throw new RuntimeException("Unexpected token in array " + token); 2631 | } 2632 | return ret; 2633 | } 2634 | 2635 | private Json readNumber() 2636 | { 2637 | int length = 0; 2638 | boolean isFloatingPoint = false; 2639 | buf.setLength(0); 2640 | 2641 | if (c == '-') 2642 | { 2643 | add(); 2644 | } 2645 | length += addDigits(); 2646 | if (c == '.') 2647 | { 2648 | add(); 2649 | length += addDigits(); 2650 | isFloatingPoint = true; 2651 | } 2652 | if (c == 'e' || c == 'E') 2653 | { 2654 | add(); 2655 | if (c == '+' || c == '-') 2656 | { 2657 | add(); 2658 | } 2659 | addDigits(); 2660 | isFloatingPoint = true; 2661 | } 2662 | 2663 | String s = buf.toString(); 2664 | Number n = isFloatingPoint 2665 | ? (length < 17) ? Double.valueOf(s) : new BigDecimal(s) 2666 | : (length < 20) ? Long.valueOf(s) : new BigInteger(s); 2667 | return factory().number(n); 2668 | } 2669 | 2670 | private int addDigits() 2671 | { 2672 | int ret; 2673 | for (ret = 0; Character.isDigit(c); ++ret) 2674 | { 2675 | add(); 2676 | } 2677 | return ret; 2678 | } 2679 | 2680 | private Json readString() 2681 | { 2682 | buf.setLength(0); 2683 | while (c != '"') 2684 | { 2685 | if (c == '\\') 2686 | { 2687 | next(); 2688 | if (c == 'u') 2689 | { 2690 | add(unicode()); 2691 | } 2692 | else 2693 | { 2694 | Object value = escapes.get(new Character(c)); 2695 | if (value != null) 2696 | { 2697 | add(((Character) value).charValue()); 2698 | } 2699 | } 2700 | } 2701 | else 2702 | { 2703 | add(); 2704 | } 2705 | } 2706 | next(); 2707 | return factory().string(buf.toString()); 2708 | } 2709 | 2710 | private void add(char cc) 2711 | { 2712 | buf.append(cc); 2713 | next(); 2714 | } 2715 | 2716 | private void add() 2717 | { 2718 | add(c); 2719 | } 2720 | 2721 | private char unicode() 2722 | { 2723 | int value = 0; 2724 | for (int i = 0; i < 4; ++i) 2725 | { 2726 | switch (next()) 2727 | { 2728 | case '0': case '1': case '2': case '3': case '4': 2729 | case '5': case '6': case '7': case '8': case '9': 2730 | value = (value << 4) + c - '0'; 2731 | break; 2732 | case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': 2733 | value = (value << 4) + (c - 'a') + 10; 2734 | break; 2735 | case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': 2736 | value = (value << 4) + (c - 'A') + 10; 2737 | break; 2738 | } 2739 | } 2740 | return (char) value; 2741 | } 2742 | } 2743 | // END Reader 2744 | 2745 | public static void main(String []argv) 2746 | { 2747 | System.out.println("JSON main"); 2748 | } 2749 | } 2750 | --------------------------------------------------------------------------------