diff options
-rw-r--r-- | src/library/scala/util/parsing/json/Parser.scala | 76 | ||||
-rw-r--r-- | test/files/run/json.check | 30 | ||||
-rw-r--r-- | test/files/run/json.scala | 88 |
3 files changed, 154 insertions, 40 deletions
diff --git a/src/library/scala/util/parsing/json/Parser.scala b/src/library/scala/util/parsing/json/Parser.scala index ce9c1dd3fe..b7c338b3be 100644 --- a/src/library/scala/util/parsing/json/Parser.scala +++ b/src/library/scala/util/parsing/json/Parser.scala @@ -19,14 +19,81 @@ import scala.util.parsing.combinator.lexical._ * * @author Derek Chen-Becker <"java"+@+"chen-becker"+"."+"org"> */ -sealed abstract class JSONType +sealed abstract class JSONType { + /** + * This version of toString allows you to provide your own value + * formatter. + */ + def toString (formatter : JSONFormat.ValueFormatter) : String + + /** + * Returns a String representation of this JSON value + * using the JSONFormat.defaultFormatter. + */ + override def toString = toString(JSONFormat.defaultFormatter) +} + +/** + * This object defines functions that are used when converting JSONType + * values into String representations. Mostly this is concerned with + * proper quoting of strings. + * + * @author Derek Chen-Becker <"java"+@+"chen-becker"+"."+"org"> + */ +object JSONFormat { + /** + * This type defines a function that can be used to + * format values into JSON format. + */ + type ValueFormatter = Any => String + + /** + * The default formatter used by the library. You can + * provide your own with the toString calls on + * JSONObject and JSONArray instances. + */ + val defaultFormatter : ValueFormatter = (x : Any) => x match { + case s : String => "\"" + quoteString(s) + "\"" + case jo : JSONObject => jo.toString(defaultFormatter) + case ja : JSONArray => ja.toString(defaultFormatter) + case other => other.toString + } + + /** + * This function can be used to properly quote Strings + * for JSON output. + */ + def quoteString (s : String) : String = + s.map { + case '"' => "\\\"" + case '\\' => "\\\\" + case '/' => "\\/" + case '\b' => "\\b" + case '\f' => "\\f" + case '\n' => "\\n" + case '\r' => "\\r" + case '\t' => "\\t" + /* We'll unicode escape any control characters. These include: + * 0x0 -> 0x1f : ASCII Control (C0 Control Codes) + * 0x7f : ASCII DELETE + * 0x80 -> 0x9f : C1 Control Codes + * + * Per RFC4627, section 2.5, we're not technically required to + * encode the C1 codes, but we do to be safe. + */ + case c if ((c >= '\u0000' && c <= '\u001f') || (c >= '\u007f' && c <= '\u009f')) => "\\u%04x".format(c: Int) + case c => c + }.mkString +} /** * Represents a JSON Object (map). + * * @author Derek Chen-Becker <"java"+@+"chen-becker"+"."+"org"> */ -case class JSONObject (obj : Map[Any,Any]) extends JSONType { - override def toString = "{" + obj.map({ case (k,v) => k + " : " + v }).mkString(", ") + "}" +case class JSONObject (obj : Map[String,Any]) extends JSONType { + def toString (formatter : JSONFormat.ValueFormatter) = + "{" + obj.map({ case (k,v) => formatter(k.toString) + " : " + formatter(v) }).mkString(", ") + "}" } /** @@ -34,7 +101,8 @@ case class JSONObject (obj : Map[Any,Any]) extends JSONType { * @author Derek Chen-Becker <"java"+@+"chen-becker"+"."+"org"> */ case class JSONArray (list : List[Any]) extends JSONType { - override def toString = "[" + list.mkString(", ") + "]" + def toString (formatter : JSONFormat.ValueFormatter) = + "[" + list.map(formatter).mkString(", ") + "]" } /** diff --git a/test/files/run/json.check b/test/files/run/json.check index 021214beaa..d4d2b41658 100644 --- a/test/files/run/json.check +++ b/test/files/run/json.check @@ -1,17 +1,21 @@ -Passed: Map(name -> value) -Passed: Map(name -> va1ue) -Passed: Map(name -> Map(name1 -> va1ue1, name2 -> va1ue2)) -Passed: Map(name -> ") -Passed: Map(function -> add_symbol) -Passed: List(Map(a -> team), Map(b -> 52.0)) -Passed: Map() -Passed: List() -Passed: List(4.0, 1.0, 3.0, 2.0, 6.0, 5.0, 8.0, 7.0) -Passed: Map(age -> 0.0) +Passed compare: {"name" : "value"} +Passed compare: {"name" : "va1ue"} +Passed compare: {"name" : {"name1" : "va1ue1", "name2" : "va1ue2"}} +Passed parse : {"name" : "\""} +Passed compare: Map(function -> add_symbol) +Passed compare: [{"a" : "team"}, {"b" : 52.0}] +Passed compare: Map() +Passed compare: List() +Passed compare: [4.0, 1.0, 3.0, 2.0, 6.0, 5.0, 8.0, 7.0] +Passed parse : {"age" : 0.0} +Passed compare: {"name" : "va1ue"} +Passed compare: {"name" : {"name1" : "va1ue1", "name2" : "va1ue2"}} +Passed compare: [4.0, 1.0, 3.0, 2.0, 6.0, 5.0, 8.0, 7.0] +Passed compare: {"\u006e\u0061\u006d\u0065" : "\u0076\u0061\u006c"} -Passed: Map(firstName -> John, lastName -> Smith, address -> Map(streetAddress -> 21 2nd Street, city -> New York, state -> NY, postalCode -> 10021.0), phoneNumbers -> List(212 732-1234, 646 123-4567)) +Passed compare: Map(firstName -> John, lastName -> Smith, address -> Map(streetAddress -> 21 2nd Street, city -> New York, state -> NY, postalCode -> 10021.0), phoneNumbers -> List(212 732-1234, 646 123-4567)) -Passed: Map(addresses -> List(Map(format -> us, type -> work, value -> 1234 Main StnSpringfield, TX 78080-1216), Map(format -> us, type -> home, value -> 5678 Main StnSpringfield, TX 78080-1316)), emailaddrs -> List(Map(type -> work, value -> kelly@seankelly.biz), Map(pref -> 1.0, type -> home, value -> kelly@seankelly.tv)), fullname -> Sean Kelly, org -> SK Consulting, telephones -> List(Map(pref -> 1.0, type -> work, value -> +1 214 555 1212), Map(type -> fax, value -> +1 214 555 1213), Map(type -> mobile, value -> +1 214 555 1214)), urls -> List(Map(type -> work, value -> http://seankelly.biz/), Map(type -> home, value -> http://seankelly.tv/))) +Passed parse : {"addresses" : [{"format" : "us", "type" : "work", "value" : "1234 Main StnSpringfield, TX 78080-1216"}, {"format" : "us", "type" : "home", "value" : "5678 Main StnSpringfield, TX 78080-1316"}], "emailaddrs" : [{"type" : "work", "value" : "kelly@seankelly.biz"}, {"pref" : 1.0, "type" : "home", "value" : "kelly@seankelly.tv"}], "fullname" : "Sean Kelly", "org" : "SK Consulting", "telephones" : [{"pref" : 1.0, "type" : "work", "value" : "+1 214 555 1212"}, {"type" : "fax", "value" : "+1 214 555 1213"}, {"type" : "mobile", "value" : "+1 214 555 1214"}], "urls" : [{"type" : "work", "value" : "http:\/\/seankelly.biz\/"}, {"type" : "home", "value" : "http:\/\/seankelly.tv\/"}]} -Passed: Map(web-app -> Map(servlet -> List(Map(init-param -> Map(cachePackageTagsRefresh -> 60.0, cachePackageTagsStore -> 200.0, cachePackageTagsTrack -> 200.0, cachePagesDirtyRead -> 10.0, cachePagesRefresh -> 10.0, cachePagesStore -> 100.0, cachePagesTrack -> 200.0, cacheTemplatesRefresh -> 15.0, cacheTemplatesStore -> 50.0, cacheTemplatesTrack -> 100.0, configGlossary:adminEmail -> ksm@pobox.com, configGlossary:installationAt -> Philadelphia, PA, configGlossary:poweredBy -> Cofax, configGlossary:poweredByIcon -> /images/cofax.gif, configGlossary:staticPath -> /content/static, dataStoreClass -> org.cofax.SqlDataStore, dataStoreConnUsageLimit -> 100.0, dataStoreDriver -> com.microsoft.jdbc.sqlserver.SQLServerDriver, dataStoreInitConns -> 10.0, dataStoreLogFile -> /usr/local/tomcat/logs/datastore.log, dataStoreLogLevel -> debug, dataStoreMaxConns -> 100.0, dataStoreName -> cofax, dataStorePassword -> dataStoreTestQuery, dataStoreTestQuery -> SET NOCOUNT ON;select test='test';, dataStoreUrl -> jdbc:microsoft:sqlserver://LOCALHOST:1433;DatabaseName=goon, dataStoreUser -> sa, defaultFileTemplate -> articleTemplate.htm, defaultListTemplate -> listTemplate.htm, jspFileTemplate -> articleTemplate.jsp, jspListTemplate -> listTemplate.jsp, maxUrlLength -> 500.0, redirectionClass -> org.cofax.SqlRedirection, searchEngineFileTemplate -> forSearchEngines.htm, searchEngineListTemplate -> forSearchEnginesList.htm, searchEngineRobotsDb -> WEB-INF/robots.db, templateLoaderClass -> org.cofax.FilesTemplateLoader, templateOverridePath -> , templatePath -> templates, templateProcessorClass -> org.cofax.WysiwygTemplate, useDataStore -> true, useJSP -> false), servlet-class -> org.cofax.cds.CDSServlet, servlet-name -> cofaxCDS), Map(init-param -> Map(mailHost -> mail1, mailHostOverride -> mail2), servlet-class -> org.cofax.cds.EmailServlet, servlet-name -> cofaxEmail), Map(servlet-class -> org.cofax.cds.AdminServlet, servlet-name -> cofaxAdmin), Map(servlet-class -> org.cofax.cds.FileServlet, servlet-name -> fileServlet), Map(init-param -> Map(adminGroupID -> 4.0, betaServer -> true, dataLog -> 1.0, dataLogLocation -> /usr/local/tomcat/logs/dataLog.log, dataLogMaxSize -> , fileTransferFolder -> /usr/local/tomcat/webapps/content/fileTransferFolder, log -> 1.0, logLocation -> /usr/local/tomcat/logs/CofaxTools.log, logMaxSize -> , lookInContext -> 1.0, removePageCache -> /content/admin/remove?cache=pages&id=, removeTemplateCache -> /content/admin/remove?cache=templates&id=, templatePath -> toolstemplates/), servlet-class -> org.cofax.cms.CofaxToolsServlet, servlet-name -> cofaxTools)), servlet-mapping -> Map(cofaxAdmin -> /admin/*, cofaxCDS -> /, cofaxEmail -> /cofaxutil/aemail/*, cofaxTools -> /tools/*, fileServlet -> /static/*), taglib -> Map(taglib-location -> /WEB-INF/tlds/cofax.tld, taglib-uri -> cofax.tld))) +Passed parse : {"web-app" : {"servlet" : [{"init-param" : {"cachePackageTagsRefresh" : 60.0, "cachePackageTagsStore" : 200.0, "cachePackageTagsTrack" : 200.0, "cachePagesDirtyRead" : 10.0, "cachePagesRefresh" : 10.0, "cachePagesStore" : 100.0, "cachePagesTrack" : 200.0, "cacheTemplatesRefresh" : 15.0, "cacheTemplatesStore" : 50.0, "cacheTemplatesTrack" : 100.0, "configGlossary:adminEmail" : "ksm@pobox.com", "configGlossary:installationAt" : "Philadelphia, PA", "configGlossary:poweredBy" : "Cofax", "configGlossary:poweredByIcon" : "\/images\/cofax.gif", "configGlossary:staticPath" : "\/content\/static", "dataStoreClass" : "org.cofax.SqlDataStore", "dataStoreConnUsageLimit" : 100.0, "dataStoreDriver" : "com.microsoft.jdbc.sqlserver.SQLServerDriver", "dataStoreInitConns" : 10.0, "dataStoreLogFile" : "\/usr\/local\/tomcat\/logs\/datastore.log", "dataStoreLogLevel" : "debug", "dataStoreMaxConns" : 100.0, "dataStoreName" : "cofax", "dataStorePassword" : "dataStoreTestQuery", "dataStoreTestQuery" : "SET NOCOUNT ON;select test='test';", "dataStoreUrl" : "jdbc:microsoft:sqlserver:\/\/LOCALHOST:1433;DatabaseName=goon", "dataStoreUser" : "sa", "defaultFileTemplate" : "articleTemplate.htm", "defaultListTemplate" : "listTemplate.htm", "jspFileTemplate" : "articleTemplate.jsp", "jspListTemplate" : "listTemplate.jsp", "maxUrlLength" : 500.0, "redirectionClass" : "org.cofax.SqlRedirection", "searchEngineFileTemplate" : "forSearchEngines.htm", "searchEngineListTemplate" : "forSearchEnginesList.htm", "searchEngineRobotsDb" : "WEB-INF\/robots.db", "templateLoaderClass" : "org.cofax.FilesTemplateLoader", "templateOverridePath" : "", "templatePath" : "templates", "templateProcessorClass" : "org.cofax.WysiwygTemplate", "useDataStore" : true, "useJSP" : false}, "servlet-class" : "org.cofax.cds.CDSServlet", "servlet-name" : "cofaxCDS"}, {"init-param" : {"mailHost" : "mail1", "mailHostOverride" : "mail2"}, "servlet-class" : "org.cofax.cds.EmailServlet", "servlet-name" : "cofaxEmail"}, {"servlet-class" : "org.cofax.cds.AdminServlet", "servlet-name" : "cofaxAdmin"}, {"servlet-class" : "org.cofax.cds.FileServlet", "servlet-name" : "fileServlet"}, {"init-param" : {"adminGroupID" : 4.0, "betaServer" : true, "dataLog" : 1.0, "dataLogLocation" : "\/usr\/local\/tomcat\/logs\/dataLog.log", "dataLogMaxSize" : "", "fileTransferFolder" : "\/usr\/local\/tomcat\/webapps\/content\/fileTransferFolder", "log" : 1.0, "logLocation" : "\/usr\/local\/tomcat\/logs\/CofaxTools.log", "logMaxSize" : "", "lookInContext" : 1.0, "removePageCache" : "\/content\/admin\/remove?cache=pages&id=", "removeTemplateCache" : "\/content\/admin\/remove?cache=templates&id=", "templatePath" : "toolstemplates\/"}, "servlet-class" : "org.cofax.cms.CofaxToolsServlet", "servlet-name" : "cofaxTools"}], "servlet-mapping" : {"cofaxAdmin" : "\/admin\/*", "cofaxCDS" : "\/", "cofaxEmail" : "\/cofaxutil\/aemail\/*", "cofaxTools" : "\/tools\/*", "fileServlet" : "\/static\/*"}, "taglib" : {"taglib-location" : "\/WEB-INF\/tlds\/cofax.tld", "taglib-uri" : "cofax.tld"}}} diff --git a/test/files/run/json.scala b/test/files/run/json.scala index 0a141dc38c..1e0b5be94b 100644 --- a/test/files/run/json.scala +++ b/test/files/run/json.scala @@ -15,65 +15,107 @@ object Test extends Application { case x => x.toString } + /* + * This method takes input JSON values and sorts keys on objects. + */ def sortJSON(in : Any) : Any = in match { case l : List[_] => l.map(sortJSON) case m : Map[String,_] => TreeMap(m.mapElements(sortJSON).elements.toSeq : _*) + // For the object versions, sort their contents, ugly casts and all... + case JSONObject(data) => JSONObject(sortJSON(data).asInstanceOf[Map[String,Any]]) + case JSONArray(data) => JSONArray(sortJSON(data).asInstanceOf[List[Any]]) case x => x } // For this one, just parsing should be considered a pass def printJSON(given : String) { - JSON parseFull given match { + JSON parseRaw given match { case None => println("Parse failed for \"%s\"".format(given)) - case Some(parsed) => println("Passed: " + sortJSON(parsed)) + case Some(parsed) => println("Passed parse : " + sortJSON(parsed)) } } - def printJSON(given : String, expected : Any) { - JSON parseFull given match { + // For this usage, do a raw parse (to JSONObject/JSONArray) + def printJSON(given : String, expected : JSONType) { + printJSON(given, JSON.parseRaw, expected) + } + + // For this usage, do a raw parse (to JSONType and subclasses) + def printJSONFull(given : String, expected : Any) { + printJSON(given, JSON.parseFull, expected) + } + + // For this usage, do configurable parsing so that you can do raw if desired + def printJSON[T](given : String, parser : String => T, expected : Any) { + parser(given) match { case None => println("Parse failed for \"%s\"".format(given)) case Some(parsed) => if (parsed == expected) { - println("Passed: " + parsed) + println("Passed compare: " + parsed) } else { val eStr = sortJSON(expected).toString val pStr = sortJSON(parsed).toString + stringDiff(eStr,pStr) + } + } + } - // Figure out where the Strings differ and generate a marker - val mismatchPosition = eStr.toList.zip(pStr.toList).findIndexOf({case (a,b) => a != b}) match { - case -1 => Math.min(eStr.length, pStr.length) + def stringDiff (expected : String, actual : String) { + if (expected != actual) { + // Figure out where the Strings differ and generate a marker + val mismatchPosition = expected.toList.zip(actual.toList).findIndexOf({case (x,y) => x != y}) match { + case -1 => Math.min(expected.length, actual.length) case x => x } val reason = (" " * mismatchPosition) + "^" - println("Expected, got:\n %s\n %s (from \"%s\")\n %s".format(eStr, pStr, given, reason)) - } + println("Expected: %s\nGot : %s \n %s".format(expected, actual, reason)) + + } else { + println("Passed compare: " + actual) } } + // The library should differentiate between lower case "l" and number "1" (ticket #136) - printJSON("{\"name\": \"value\"}", Map("name" -> "value")) - printJSON("{\"name\": \"va1ue\"}", Map("name" -> "va1ue")) - printJSON("{\"name\": { \"name1\": \"va1ue1\", \"name2\": \"va1ue2\" } }", - Map("name" -> Map("name1" -> "va1ue1", "name2" -> "va1ue2"))) + printJSON("{\"name\" : \"value\"}", JSONObject(Map("name" -> "value"))) + printJSON("{\"name\" : \"va1ue\"}", JSONObject(Map("name" -> "va1ue"))) + printJSON("{\"name\" : { \"name1\" : \"va1ue1\", \"name2\" : \"va1ue2\" } }", + JSONObject(Map("name" -> JSONObject(Map("name1" -> "va1ue1", "name2" -> "va1ue2"))))) // Unicode escapes should be handled properly - printJSON("{\"name\": \"\\u0022\"}") + printJSON("{\"name\" : \"\\u0022\"}") // The library should return a map for JSON objects (ticket #873) - printJSON("""{"function":"add_symbol"}""", Map("function" -> "add_symbol")) + printJSONFull("{\"function\" : \"add_symbol\"}", Map("function" -> "add_symbol")) // The library should recurse into arrays to find objects (ticket #2207) - printJSON("""[{"a": "team"},{"b": 52}]""", List(Map("a" -> "team"), Map("b" -> 52.0))) + printJSON("[{\"a\" : \"team\"},{\"b\" : 52}]", JSONArray(List(JSONObject(Map("a" -> "team")), JSONObject(Map("b" -> 52.0))))) // The library should differentiate between empty maps and lists (ticket #3284) - printJSON("{}", Map()) - printJSON("[]", List()) + printJSONFull("{}", Map()) + printJSONFull("[]", List()) // Lists should be returned in the same order as specified - printJSON("[4,1,3,2,6,5,8,7]", List[Double](4,1,3,2,6,5,8,7)) + printJSON("[4,1,3,2,6,5,8,7]", JSONArray(List[Double](4,1,3,2,6,5,8,7))) // Additional tests printJSON("{\"age\": 0}") + // The library should do a proper toString representation using default and custom renderers (ticket #3605) + stringDiff("{\"name\" : \"va1ue\"}", JSONObject(Map("name" -> "va1ue")).toString) + stringDiff("{\"name\" : {\"name1\" : \"va1ue1\", \"name2\" : \"va1ue2\"}}", + JSONObject(Map("name" -> JSONObject(TreeMap("name1" -> "va1ue1", "name2" -> "va1ue2")))).toString) + + stringDiff("[4.0, 1.0, 3.0, 2.0, 6.0, 5.0, 8.0, 7.0]", JSONArray(List[Double](4,1,3,2,6,5,8,7)).toString) + + // A test method that escapes all characters in strings + def escapeEverything (in : Any) : String = in match { + case s : String => "\"" + s.map(c => "\\u%04x".format(c : Int)).mkString + "\"" + case jo : JSONObject => jo.toString(escapeEverything) + case ja : JSONArray => ja.toString(escapeEverything) + case other => other.toString + } + + stringDiff("{\"\\u006e\\u0061\\u006d\\u0065\" : \"\\u0076\\u0061\\u006c\"}", JSONObject(Map("name" -> "val")).toString(escapeEverything)) println @@ -111,7 +153,7 @@ object Test extends Application { ) - printJSON(sample1, sample1Obj) + printJSONFull(sample1, sample1Obj) println // from http://www.developer.com/lang/jscript/article.php/3596836 @@ -139,7 +181,7 @@ object Test extends Application { {"type": "home", "value": "http://seankelly.tv/"} ] }""" - //println(sample2) + printJSON(sample2) println @@ -235,7 +277,7 @@ object Test extends Application { "taglib-location": "/WEB-INF/tlds/cofax.tld"} } }""" - //println(sample3) + printJSON(sample3) println } |