├── slurp ├── src │ └── main │ │ ├── resources │ │ └── reference.conf │ │ ├── scala │ │ ├── morph │ │ │ ├── extractor │ │ │ │ ├── Extractor.scala │ │ │ │ ├── AstTransformer.scala │ │ │ │ └── Interface.scala │ │ │ ├── parser │ │ │ │ ├── ParsingException.scala │ │ │ │ ├── AstBuilder.scala │ │ │ │ ├── WhiteSpaceExpansion.scala │ │ │ │ ├── XmlParser.scala │ │ │ │ ├── Interface.scala │ │ │ │ ├── BaseParser.scala │ │ │ │ ├── CsvParser.scala │ │ │ │ └── JsonParser.scala │ │ │ ├── utils │ │ │ │ └── Utils.scala │ │ │ └── ast │ │ │ │ ├── Ast.scala │ │ │ │ └── DSL.scala │ │ ├── slurp │ │ │ ├── QueueDumper.scala │ │ │ ├── Slurp.scala │ │ │ └── GitHubStream.scala │ │ └── Counter.scala │ │ └── java │ │ ├── org │ │ └── json │ │ │ ├── JSONString.java │ │ │ ├── JSONException.java │ │ │ ├── HTTPTokener.java │ │ │ ├── Property.java │ │ │ ├── JSONStringer.java │ │ │ ├── CookieList.java │ │ │ ├── HTTP.java │ │ │ ├── Cookie.java │ │ │ ├── CDL.java │ │ │ ├── JSONWriter.java │ │ │ ├── XMLTokener.java │ │ │ ├── Kim.java │ │ │ ├── JSONTokener.java │ │ │ ├── JSONML.java │ │ │ └── XML.java │ │ └── DeliveryBoy.java ├── .gitignore ├── application.example.conf └── build.sbt ├── cartographer ├── public │ ├── .gitignore │ ├── favicon.ico │ ├── media │ │ └── tooltip.png │ ├── index.html │ ├── css │ │ └── gitlive.css │ └── gitlive.js ├── cache │ └── .gitignore ├── .gitignore ├── README.md ├── readyComponents ├── bower.json ├── package.json └── app.js ├── runCartographer ├── runSlurp ├── README.md └── LICENSE.md /slurp/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cartographer/public/.gitignore: -------------------------------------------------------------------------------- 1 | components/ 2 | -------------------------------------------------------------------------------- /cartographer/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /slurp/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | application.conf 3 | -------------------------------------------------------------------------------- /cartographer/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | -------------------------------------------------------------------------------- /cartographer/README.md: -------------------------------------------------------------------------------- 1 | # Cartographer 2 | 3 | Real-time GitHub Data Visualization 4 | -------------------------------------------------------------------------------- /cartographer/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anishathalye/gitlive/HEAD/cartographer/public/favicon.ico -------------------------------------------------------------------------------- /cartographer/public/media/tooltip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anishathalye/gitlive/HEAD/cartographer/public/media/tooltip.png -------------------------------------------------------------------------------- /slurp/application.example.conf: -------------------------------------------------------------------------------- 1 | github { 2 | clientId = xxxxxxxxxxxxxxxxxxxx 3 | clientSecret = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 4 | } 5 | -------------------------------------------------------------------------------- /runCartographer: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | BASEDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 6 | 7 | cd $BASEDIR/cartographer 8 | 9 | forever --minUptime 500ms --spinSleepTime 1000ms app.js 10 | -------------------------------------------------------------------------------- /cartographer/readyComponents: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | BASEDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 6 | 7 | cd $BASEDIR 8 | 9 | mkdir -p public/components 10 | 11 | cp bower_components/datamaps/dist/datamaps.world.min.js public/components/datamaps.world.min.js 12 | cp bower_components/d3-tip/index.js public/components/d3-tip.js 13 | -------------------------------------------------------------------------------- /cartographer/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cartographer", 3 | "homepage": "https://github.com/anishathalye/gh-vis-challenge", 4 | "private": true, 5 | "ignore": [ 6 | "**/.*", 7 | "node_modules", 8 | "public/components", 9 | "test", 10 | "tests" 11 | ], 12 | "dependencies": { 13 | "datamaps": "~0.3.3", 14 | "d3-tip": "~0.6.4" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /runSlurp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | BASEDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 6 | 7 | slurp() { 8 | ( 9 | cd $BASEDIR/slurp 10 | sbt 'runMain slurp.Slurp' 11 | ) 12 | } 13 | 14 | while [ 1 ]; do 15 | echo 'Running slurp...' 16 | slurp 17 | if [ $? == 130 ]; then 18 | exit 130 19 | fi 20 | done 21 | -------------------------------------------------------------------------------- /cartographer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Cartographer", 3 | "description": "Real-time GitHub Data Visualization", 4 | "repository": "https://github.com/anishathalye/gh-vis-challenge", 5 | "version": "1.0.0", 6 | "dependencies": { 7 | "compression": "^1.0.11", 8 | "express": "^4.8.4", 9 | "express-minify": "0.0.11", 10 | "rabbit.js": "^0.4.1" 11 | }, 12 | "engine": "node >= 0.10" 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHub Data Visualization Challenge 2014 2 | 3 | This is no longer hosted on the internet. For more info, see my [blog][blog]. 4 | 5 | Made with ♥ by Anish Athalye ([anishathalye][anishathalye]). 6 | 7 | The code is a bit of a mess. Use it at your own risk. 8 | 9 | License 10 | ------- 11 | 12 | Copyright (c) 2014 Anish Athalye. Released under the MIT License. See 13 | [LICENSE.md][license] for details. 14 | 15 | [blog]: http://www.anishathalye.com/2015/07/07/bye-git-live/ 16 | [anishathalye]: https://github.com/anishathalye 17 | [license]: LICENSE.md 18 | -------------------------------------------------------------------------------- /slurp/build.sbt: -------------------------------------------------------------------------------- 1 | name := "slurp" 2 | 3 | scalaVersion := "2.11.2" 4 | 5 | scalacOptions := Seq( 6 | "-unchecked", "-deprecation", "-feature", "-Xfatal-warnings" 7 | ) 8 | 9 | unmanagedClasspath in Runtime += baseDirectory.value 10 | 11 | libraryDependencies ++= Seq( 12 | "org.scala-lang.modules" %% "scala-async" % "0.9.2", 13 | "com.typesafe" % "config" % "1.2.1", 14 | "org.parboiled" %% "parboiled-scala" % "1.1.6", 15 | "net.databinder.dispatch" %% "dispatch-core" % "0.11.2", 16 | "com.rabbitmq" % "amqp-client" % "3.3.5", 17 | "com.twitter" %% "util-collection" % "6.20.0" 18 | ) 19 | -------------------------------------------------------------------------------- /slurp/src/main/scala/morph/extractor/Extractor.scala: -------------------------------------------------------------------------------- 1 | package morph.extractor 2 | 3 | import morph.ast._ 4 | 5 | /** 6 | * The base class that all extractors should extend. 7 | * 8 | * This class mixes in the DSL and AST-related implicit conversions 9 | * so that subclasses can omit those imports. 10 | * 11 | * Subclasses must define the extract method (inherited from AstTransformer), 12 | * which takes a ValueNode and performs extractions / transformations on it. 13 | * 14 | * @author Anish Athalye 15 | */ 16 | abstract class Extractor extends AstTransformer with DSL with Implicits 17 | -------------------------------------------------------------------------------- /slurp/src/main/scala/morph/parser/ParsingException.scala: -------------------------------------------------------------------------------- 1 | package morph.parser 2 | 3 | /** 4 | * An exception that parsers throw if they encounter an error while parsing. 5 | * 6 | * @author Anish Athalye 7 | */ 8 | class ParsingException(message: String = null, cause: Throwable = null) 9 | extends RuntimeException(message, cause) 10 | 11 | object ParsingException { 12 | 13 | def apply() = new ParsingException(null, null) 14 | 15 | def apply(msg: String) = new ParsingException(msg, null) 16 | 17 | def apply(msg: String, cause: Throwable) = new ParsingException(msg, cause) 18 | 19 | def apply(cause: Throwable) = new ParsingException(null, cause) 20 | } 21 | -------------------------------------------------------------------------------- /slurp/src/main/java/org/json/JSONString.java: -------------------------------------------------------------------------------- 1 | package org.json; 2 | /** 3 | * The JSONString interface allows a toJSONString() 4 | * method so that a class can change the behavior of 5 | * JSONObject.toString(), JSONArray.toString(), 6 | * and JSONWriter.value(Object). The 7 | * toJSONString method will be used instead of the default behavior 8 | * of using the Object's toString() method and quoting the result. 9 | */ 10 | public interface JSONString { 11 | /** 12 | * The toJSONString method allows a class to produce its own JSON 13 | * serialization. 14 | * 15 | * @return A strictly syntactically correct JSON text. 16 | */ 17 | public String toJSONString(); 18 | } 19 | -------------------------------------------------------------------------------- /slurp/src/main/scala/morph/extractor/AstTransformer.scala: -------------------------------------------------------------------------------- 1 | package morph.extractor 2 | 3 | import morph.ast._ 4 | 5 | /** 6 | * A trait describing methods that extractors must implement. 7 | * 8 | * The rest of the framework expects extractors to implement this trait. 9 | * 10 | * @author Anish Athalye 11 | */ 12 | trait AstTransformer { 13 | 14 | /** 15 | * The main extraction / transformation method. 16 | * 17 | * @param node The node to transform. 18 | * 19 | * @return A transformed node. 20 | */ 21 | final def apply(node: ValueNode): ValueNode = extract(node) 22 | 23 | /** 24 | * The main extraction / transformation method. 25 | * 26 | * @param node The node to transform. 27 | * 28 | * @return A transformed node. 29 | */ 30 | def extract(node: ValueNode): ValueNode 31 | } 32 | -------------------------------------------------------------------------------- /slurp/src/main/scala/morph/parser/AstBuilder.scala: -------------------------------------------------------------------------------- 1 | package morph.parser 2 | 3 | import morph.ast._ 4 | 5 | /** 6 | * A trait describing methods that AST builders must implement. 7 | * 8 | * The rest of the framework expects parsers to implement this trait, 9 | * so all custom parsers should implement this trait. 10 | * 11 | * @author Anish Athalye 12 | */ 13 | trait AstBuilder { 14 | 15 | /** 16 | * The main parsing method. 17 | * 18 | * @param input The content to parse. 19 | * 20 | * @return The root of the generated AST. 21 | */ 22 | def apply(input: String): ValueNode 23 | 24 | /** 25 | * The main parsing method. 26 | * 27 | * @param input The content to parse. 28 | * 29 | * @return The root of the generated AST. 30 | */ 31 | def apply(input: Array[Char]): ValueNode 32 | } 33 | -------------------------------------------------------------------------------- /slurp/src/main/scala/slurp/QueueDumper.scala: -------------------------------------------------------------------------------- 1 | package slurp 2 | 3 | import java.util.concurrent.BlockingQueue 4 | 5 | import com.rabbitmq.client._ 6 | 7 | class QueueDumper(val queue: BlockingQueue[String]) { 8 | 9 | def createConnection() { 10 | val factory = new ConnectionFactory(); 11 | factory.setHost("localhost"); 12 | val connection = factory.newConnection(); 13 | channel = connection.createChannel(); 14 | channel.exchangeDeclare(EXCHANGE_NAME, "fanout") 15 | } 16 | 17 | def start() { 18 | createConnection() 19 | val thread = new Thread(new Runnable { 20 | override def run { 21 | loop() 22 | } 23 | }) 24 | thread.start() 25 | } 26 | 27 | def loop() { 28 | while (true) { 29 | val msg = queue.take() 30 | channel.basicPublish(EXCHANGE_NAME, "", null, msg.getBytes) 31 | } 32 | } 33 | 34 | private var channel: Channel = null 35 | 36 | private val EXCHANGE_NAME = "gh-events" 37 | 38 | private val HOST = "localhost" 39 | 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | **Copyright (c) 2014 Anish Athalye (me@anishathalye.com)** 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 | of the Software, and to permit persons to whom the Software is furnished to do 11 | so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /slurp/src/main/java/org/json/JSONException.java: -------------------------------------------------------------------------------- 1 | package org.json; 2 | 3 | /** 4 | * The JSONException is thrown by the JSON.org classes when things are amiss. 5 | * 6 | * @author JSON.org 7 | * @version 2013-02-10 8 | */ 9 | public class JSONException extends RuntimeException { 10 | private static final long serialVersionUID = 0; 11 | private Throwable cause; 12 | 13 | /** 14 | * Constructs a JSONException with an explanatory message. 15 | * 16 | * @param message 17 | * Detail about the reason for the exception. 18 | */ 19 | public JSONException(String message) { 20 | super(message); 21 | } 22 | 23 | /** 24 | * Constructs a new JSONException with the specified cause. 25 | */ 26 | public JSONException(Throwable cause) { 27 | super(cause.getMessage()); 28 | this.cause = cause; 29 | } 30 | 31 | /** 32 | * Returns the cause of this exception or null if the cause is nonexistent 33 | * or unknown. 34 | * 35 | * @returns the cause of this exception or null if the cause is nonexistent 36 | * or unknown. 37 | */ 38 | public Throwable getCause() { 39 | return this.cause; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /slurp/src/main/scala/Counter.scala: -------------------------------------------------------------------------------- 1 | import collection.mutable.{ Set => MSet } 2 | 3 | import com.rabbitmq.client._ 4 | 5 | import morph.ast._, DSL._, Implicits._ 6 | import morph.parser._ 7 | 8 | object Counter { 9 | 10 | def main(args: Array[String]) { 11 | val factory = new ConnectionFactory() 12 | factory.setHost("localhost") 13 | val connection = factory.newConnection() 14 | val channel = connection.createChannel() 15 | 16 | channel.exchangeDeclare(EXCHANGE_NAME, "fanout") 17 | val queueName = channel.queueDeclare().getQueue() 18 | channel.queueBind(queueName, EXCHANGE_NAME, "") 19 | 20 | val consumer = new QueueingConsumer(channel) 21 | channel.basicConsume(queueName, true, consumer) 22 | 23 | val locs = MSet[String]() 24 | 25 | val startTime = System.nanoTime 26 | var messages = 0 27 | 28 | while (true) { 29 | val delivery = consumer.nextDelivery() 30 | val message = new String(delivery.getBody()) 31 | val json = JsonParser(message) 32 | val loc = (json ~> "location") foreach { l => 33 | locs += l.asString 34 | messages += 1 35 | val time = (System.nanoTime - startTime) / 1000000000 36 | println(s"${locs.size} unique, ${messages} total in ${time} seconds") 37 | } 38 | } 39 | } 40 | 41 | val EXCHANGE_NAME = "gh-events" 42 | 43 | } 44 | -------------------------------------------------------------------------------- /slurp/src/main/scala/morph/extractor/Interface.scala: -------------------------------------------------------------------------------- 1 | package morph.extractor 2 | 3 | import morph.ast._ 4 | 5 | import io.Source 6 | 7 | /** 8 | * An interface to make using extractors easy. 9 | * 10 | * This interface makes calling extractors look like part of 11 | * the DSL. 12 | * 13 | * @example 14 | * {{{ 15 | * scala> val transformed = transform node dataNode using DataNodeExtractor 16 | * transformed: morph.ast.ValueNode = ... 17 | * }}} 18 | * 19 | * @author Anish Athalye 20 | */ 21 | object Interface { 22 | 23 | /** 24 | * An object to help transform various data sources. 25 | */ 26 | object transform { 27 | 28 | /** 29 | * Transform a node. 30 | * 31 | * @param node The node to transform. 32 | * 33 | * @return A Transformable instance that can be transformed using an Extractor. 34 | */ 35 | def node(node: ValueNode): Transformable = new Transformable(node) 36 | } 37 | 38 | /** 39 | * A class that holds ready-to-transform data. 40 | */ 41 | class Transformable(node: ValueNode) { 42 | 43 | /** 44 | * Transform data using a specific extractor. 45 | * 46 | * @param extractor The extractor to use. 47 | * 48 | * @return The root of the transformed AST. 49 | */ 50 | def using(extractor: AstTransformer): ValueNode = extractor(node) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /slurp/src/main/scala/slurp/Slurp.scala: -------------------------------------------------------------------------------- 1 | package slurp 2 | 3 | import java.util.concurrent.{ BlockingQueue, LinkedBlockingQueue } 4 | 5 | import com.typesafe.config.ConfigFactory 6 | 7 | import dispatch._, Defaults._ 8 | 9 | import morph.ast._, DSL._, Implicits._ 10 | 11 | object Slurp { 12 | 13 | def main(args: Array[String]) { 14 | val config = ConfigFactory.load() 15 | val clientId = config.getString("github.clientId") 16 | val clientSecret = config.getString("github.clientSecret") 17 | val ghs = new GitHubStream(clientId, clientSecret) 18 | val queue = new LinkedBlockingQueue[String]() 19 | val qd = new QueueDumper(queue) 20 | qd.start() 21 | mainLoop(ghs, queue) 22 | } 23 | 24 | def mainLoop(ghs: GitHubStream, queue: BlockingQueue[String]) { 25 | while (true) { 26 | try { 27 | val (events, pollInterval) = ghs.getEvents() 28 | val timeStep = pollInterval * 1000 29 | if (events.nonEmpty) { 30 | events foreach { event => 31 | queue.offer(event) 32 | Thread.sleep(timeStep) 33 | } 34 | } else { 35 | Thread.sleep(EMPTY_SLEEP) 36 | } 37 | } catch { 38 | case e: Exception => { 39 | e.printStackTrace() 40 | Thread.sleep(EMPTY_SLEEP) 41 | } 42 | } 43 | } 44 | } 45 | 46 | private val EMPTY_SLEEP = 5000 47 | 48 | } 49 | -------------------------------------------------------------------------------- /slurp/src/main/java/DeliveryBoy.java: -------------------------------------------------------------------------------- 1 | import java.io.IOException; 2 | import com.rabbitmq.client.ConnectionFactory; 3 | import com.rabbitmq.client.Connection; 4 | import com.rabbitmq.client.Channel; 5 | import com.rabbitmq.client.QueueingConsumer; 6 | 7 | public class DeliveryBoy { 8 | 9 | private static final String EXCHANGE_NAME = "gh-events"; 10 | 11 | public static void main(String[] argv) 12 | throws java.io.IOException, 13 | java.lang.InterruptedException { 14 | 15 | ConnectionFactory factory = new ConnectionFactory(); 16 | factory.setHost("localhost"); 17 | Connection connection = factory.newConnection(); 18 | Channel channel = connection.createChannel(); 19 | 20 | channel.exchangeDeclare(EXCHANGE_NAME, "fanout"); 21 | String queueName = channel.queueDeclare().getQueue(); 22 | channel.queueBind(queueName, EXCHANGE_NAME, ""); 23 | 24 | System.out.println(" [*] Waiting for messages. To exit press CTRL+C"); 25 | 26 | QueueingConsumer consumer = new QueueingConsumer(channel); 27 | channel.basicConsume(queueName, true, consumer); 28 | 29 | while (true) { 30 | QueueingConsumer.Delivery delivery = consumer.nextDelivery(); 31 | String message = new String(delivery.getBody()); 32 | 33 | System.out.println(" [x] Received '" + message + "'"); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /slurp/src/main/scala/morph/parser/WhiteSpaceExpansion.scala: -------------------------------------------------------------------------------- 1 | package morph.parser 2 | 3 | import scala.language.implicitConversions 4 | 5 | import org.parboiled.scala._ 6 | 7 | /** 8 | * A mixin for Parsers that makes matching whitespace easier. 9 | * 10 | * When this trait is mixed in, all strings of the form `"text "` 11 | * (note the trailing space) are automatically converted to a 12 | * rule that matches all trailing whitespace characters as well. 13 | * Whitespace characters are defined as any of: 14 | * `' '`, `'\n'`, `'\r'`, `'\t'`, `'\f'`, but this behavior can 15 | * be overridden. 16 | * 17 | * @author Anish Athalye 18 | */ 19 | trait WhiteSpaceExpansion { 20 | 21 | // self type, class mixing this in must be a Parser 22 | this: Parser => 23 | 24 | /** 25 | * Rule that matches all whitespace. 26 | * 27 | * Matches all whitespace including `' '` (space), `'\n'` (newline), 28 | * `'\r'` (carriage return), `'\t'` (tab), and `'\f'` (form feed). 29 | */ 30 | def WhiteSpace: Rule0 = rule { zeroOrMore(anyOf(" \n\r\t\f")) } 31 | 32 | /** 33 | * An implicit conversion to make writing whitespace matching rules 34 | * less tedious. 35 | * 36 | * When converting a string to a rule, if the string ends with a space, 37 | * this automatically converts it to a rule that matches any whitespace 38 | * after the string. 39 | */ 40 | override implicit def toRule(string: String) = { 41 | if (string.endsWith(" ")) { 42 | str(string.trim) ~ WhiteSpace 43 | } else { 44 | str(string) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /slurp/src/main/scala/morph/parser/XmlParser.scala: -------------------------------------------------------------------------------- 1 | package morph.parser 2 | 3 | import morph.ast._ 4 | 5 | import org.json.{ XML, JSONException } 6 | 7 | /** 8 | * An XML parser that constructs an AST. 9 | * 10 | * Instead of implementing an XML parser from scratch, this 11 | * parser uses an already existing implementation of an XML 12 | * parser to convert XML to JSON, and then uses the 13 | * [[morph.parser.JsonParser]] to convert that to a native AST. 14 | * 15 | * This parser follows the guidelines available at 16 | * www.xml.com/pub/a/2006/05/31/converting-between-xml-and-json.html 17 | * to convert XML to the native AST format. 18 | * 19 | * XML attributes are prepended with an `@`, and content can be found 20 | * in `#text`. 21 | * 22 | * Arrays will be transformed to JSON arrays. 23 | * 24 | * @author Anish Athalye 25 | */ 26 | object XmlParser extends AstBuilder { 27 | 28 | /** 29 | * The main parsing method. 30 | * 31 | * @param input The content to parse. 32 | * 33 | * @return The root of the generated AST. 34 | */ 35 | def apply(input: String) = { 36 | try { 37 | val json = XML toJSONObject input toString 0 38 | JsonParser(json) // to get the XML in native AST form 39 | } catch { 40 | case ex: JSONException => throw ParsingException(ex.getMessage) 41 | } 42 | } 43 | 44 | /** 45 | * The main parsing method. 46 | * 47 | * @param input The content to parse. 48 | * 49 | * @return The root of the generated AST. 50 | */ 51 | def apply(input: Array[Char]) = apply(input.mkString) 52 | } 53 | -------------------------------------------------------------------------------- /slurp/src/main/scala/morph/parser/Interface.scala: -------------------------------------------------------------------------------- 1 | package morph.parser 2 | 3 | import morph.ast._ 4 | 5 | import io.Source 6 | 7 | /** 8 | * An interface to make using parsers easy. 9 | * 10 | * This interface makes calling parsers look like part of 11 | * the DSL. 12 | * 13 | * @example 14 | * {{{ 15 | * scala> val root = parse file "data.xml" using XmlParser 16 | * root: morph.ast.ValueNode = ... 17 | * }}} 18 | * 19 | * @author Anish Athalye 20 | */ 21 | object Interface { 22 | 23 | /** 24 | * An object to help parse various data sources. 25 | */ 26 | object parse { 27 | 28 | /** 29 | * Parse a string. 30 | * 31 | * @param str The string to parse. 32 | * 33 | * @return A Parsable instance that can be parsed using an AstBuilder. 34 | */ 35 | def string(str: String): Parsable = new Parsable(str) 36 | 37 | /** 38 | * Parse a file specified by a given path. 39 | * 40 | * @param path The path of the file to parse. 41 | * 42 | * @throws FileNotFoundException If the file cannot be accessed. 43 | * 44 | * @return A Parsable instance that can be parsed using an AstBuilder. 45 | */ 46 | def file(path: String): Parsable = { 47 | val source = Source fromFile path 48 | val data = source.getLines mkString "\n" 49 | source.close() 50 | new Parsable(data) 51 | } 52 | } 53 | 54 | /** 55 | * A class that holds ready-to-parse data. 56 | */ 57 | class Parsable(data: String) { 58 | 59 | /** 60 | * Parse data using a specific parser. 61 | * 62 | * @param parser The parser to use. 63 | * 64 | * @throws ParsingException If an error occurs during parsing. 65 | * 66 | * @return The root of the AST generated by parsing the data. 67 | */ 68 | def using(parser: AstBuilder): ValueNode = parser(data) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /slurp/src/main/scala/morph/parser/BaseParser.scala: -------------------------------------------------------------------------------- 1 | package morph.parser 2 | 3 | import morph.ast._ 4 | 5 | import org.parboiled.scala._ 6 | import org.parboiled.errors.ErrorUtils 7 | 8 | /** 9 | * A base class for parsers. 10 | * 11 | * This class provides the boilerplate code that all Parboiled 12 | * parsers will need. Subclasses only need to provide the 13 | * parboiled rules for transforming input into an AST. 14 | * 15 | * @author Anish Athalye 16 | */ 17 | abstract class BaseParser extends Parser with AstBuilder { 18 | 19 | /** 20 | * The root parsing rule. 21 | * 22 | * This is the rule that will be run when there is input 23 | * to be parsed. It should return the root of the generated AST. 24 | */ 25 | def RootRule: Rule1[ValueNode] 26 | 27 | /** 28 | * The main parsing method. 29 | * 30 | * If an error is encountered when parsing, this method does not 31 | * make any attempt to continue. 32 | * 33 | * @param input The content to parse. 34 | * 35 | * @throws ParsingException If the parser encounters an error while parsing. 36 | * 37 | * @return The root of the generated AST. 38 | */ 39 | def apply(input: String): ValueNode = apply(input.toCharArray) 40 | 41 | /** 42 | * The main parsing method. 43 | * 44 | * If an error is encountered when parsing, this method does not 45 | * make any attempt to continue. 46 | * 47 | * @param input The content to parse. 48 | * 49 | * @throws ParsingException If the parser encounters an error while parsing. 50 | * 51 | * @return The root of the generated AST. 52 | */ 53 | def apply(input: Array[Char]): ValueNode = { 54 | val parsingResult = ReportingParseRunner(RootRule).run(input) 55 | parsingResult.result getOrElse { 56 | throw ParsingException("Invalid source:\n" + 57 | ErrorUtils.printParseErrors(parsingResult)) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /slurp/src/main/scala/morph/parser/CsvParser.scala: -------------------------------------------------------------------------------- 1 | package morph.parser 2 | 3 | import morph.ast._ 4 | 5 | import org.parboiled.scala._ 6 | import org.parboiled.Context 7 | 8 | import scala.collection.mutable.Builder 9 | 10 | /** 11 | * A CSV parser that constructs an AST. 12 | * 13 | * This parser can parse CSV files that conform to the RFC 4180 spec. 14 | * This parser was implemented almost directly from the ABNF grammar 15 | * found in the RFC. Note that input to this parser must be valid 16 | * according to the spec to be able to be correctly parsed. 17 | * 18 | * @author Anish Athalye 19 | */ 20 | object CsvParser extends BaseParser { 21 | 22 | def RootRule = Csv 23 | 24 | lazy val Csv = rule { File ~ optional(CRLF) ~ EOI } 25 | 26 | def File = rule { FileUnwrapped ~~> { ArrayNode(_) } } 27 | 28 | def FileUnwrapped = rule { Records ~~> { _.result } } 29 | 30 | def Records = rule { 31 | push(Vector.newBuilder[ArrayNode]) ~ oneOrMore(rule { 32 | Record ~~% { withContext(appendToVb(_: ArrayNode, _)) } 33 | }, separator = CRLF) 34 | } 35 | 36 | def Record = rule { RecordUnwrapped ~~> { ArrayNode(_) } } 37 | 38 | def RecordUnwrapped = rule { Fields ~~> { _.result } } 39 | 40 | def Fields = rule { 41 | push(Vector.newBuilder[StringNode]) ~ oneOrMore(rule { 42 | Field ~~% { withContext(appendToVb(_: StringNode, _)) } 43 | }, separator = COMMA) 44 | } 45 | 46 | def Field = rule { Escaped | NonEscaped } 47 | 48 | def Escaped = rule { 49 | DQUOTE ~ 50 | zeroOrMore(TEXTDATA | COMMA | CR | LF | DDQUOTE) ~> 51 | { s => StringNode(unDDQUOTE(s)) } ~ 52 | DQUOTE 53 | } 54 | 55 | def NonEscaped = rule { 56 | zeroOrMore(TEXTDATA) ~> { s => StringNode(s) } 57 | } 58 | 59 | def unDDQUOTE(s: String) = s.replace(DDQUOTE, DQUOTE) 60 | 61 | def COMMA = "," 62 | 63 | def CR = "\r" 64 | 65 | def LF = "\n" 66 | 67 | def CRLF = rule { LF | CR + LF } 68 | 69 | def DQUOTE = "\"" 70 | 71 | def DDQUOTE = "\"\"" 72 | 73 | def TEXTDATA = rule { " " - "!" | "#" - "+" | "-" - "~" } 74 | 75 | def appendToVb[T](e: T, ctx: Context[_]) { 76 | ctx.getValueStack.peek.asInstanceOf[Builder[T, Vector[T]]] += e 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /slurp/src/main/scala/morph/utils/Utils.scala: -------------------------------------------------------------------------------- 1 | package morph.utils 2 | 3 | /** 4 | * A collection of various utilities. 5 | * 6 | * Most utilities are in the form of implicit classes contained 7 | * inside the `Utils` object. 8 | * 9 | * @author Anish Athalye 10 | */ 11 | object Utils { 12 | 13 | /** 14 | * An implicit class to provide additional methods on `String`. 15 | */ 16 | implicit class StringUtils(val str: String) extends AnyVal { 17 | 18 | /** 19 | * Indent every line in the by two spaces. 20 | * 21 | * @note This works as expected with both `"\n"` and `"\r\n"` newlines. 22 | * 23 | * @return The indented string. 24 | */ 25 | def indent: String = str indent 2 26 | 27 | /** 28 | * Indent every line in a string by a specified number of spaces. 29 | * 30 | * @note This works as expected with both `"\n"` and `"\r\n"` newlines. 31 | * 32 | * @param num The number of spaces to indent every line by. 33 | * 34 | * @return The indented string. 35 | */ 36 | def indent(num: Int): String = 37 | (" " * num) + str.replace("\n", "\n" + " " * num) 38 | 39 | /** 40 | * Escape special characters with backslash escapes. 41 | * 42 | * This escapes the special charactesr in the string so that the string 43 | * will be suitable for being displayed as a double quoted string. 44 | * 45 | * The escape codes specified in JLS 3.10.6 are implemented, with the 46 | * exception of the single quote character (which does not need to be 47 | * escaped in a double quoted string). 48 | * 49 | * @example 50 | * {{{ 51 | * scala> val str = """hello\ 52 | * | world""" 53 | * str: String = 54 | * hello\ 55 | * world 56 | * 57 | * scala> val escaped = str.escaped 58 | * escaped: String = hello\\\nworld 59 | * }}} 60 | * 61 | * @return The string with escapes visibly escaped. 62 | */ 63 | def escaped: String = str flatMap { 64 | case '\b' => "\\b" 65 | case '\f' => "\\f" 66 | case '\n' => "\\n" 67 | case '\r' => "\\r" 68 | case '\t' => "\\t" 69 | case '\\' => "\\\\" 70 | case '"' => "\\\"" 71 | case c => c.toString 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /cartographer/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var compression = require('compression'); 3 | var minify = require('express-minify'); 4 | var rabbit = require('rabbit.js'); 5 | var connections = []; 6 | 7 | var context = rabbit.createContext(); 8 | context.on('ready', function() { 9 | console.log('Connection ready'); 10 | var sub = context.socket('SUB'); 11 | sub.setEncoding('utf-8'); 12 | sub.connect('gh-events', '', function() { 13 | console.log('Subscribed') 14 | sub.on('data', function(data) { 15 | consume(data); 16 | }); 17 | }); 18 | }); 19 | 20 | app = express(); 21 | app.all(/.*/, function(req, res, next) { 22 | var host = req.header("host"); 23 | if (host.match(/^www\..*/i)) { 24 | next(); 25 | } else { 26 | res.redirect(301, "http://www." + host); 27 | } 28 | }); 29 | app.use(compression()); 30 | app.use(minify({cache: __dirname + '/cache'})); 31 | app.use(express.static(__dirname + '/public')); 32 | 33 | app.get('/events', function(req, res) { 34 | if (req.headers.accept == 'text/event-stream') { 35 | res.writeHead(200, { 36 | 'content-type': 'text/event-stream', 37 | 'cache-control': 'no-cache', 38 | 'connection': 'keep-alive' 39 | }); 40 | connections.push(res); 41 | console.log('Connection added for ' + req.ip); 42 | 43 | req.on('close', function () { 44 | removeConnection(res); 45 | }); 46 | } else { 47 | res.status(500).send('This path for EventSource subscription only...'); 48 | } 49 | }); 50 | 51 | app.listen(8000); 52 | 53 | function consume(message) { 54 | broadcast(JSON.stringify(message)); 55 | } 56 | 57 | function broadcast(data) { 58 | var id = (new Date()).toLocaleTimeString(); 59 | connections.forEach(function (res) { 60 | writeEvent(res, id, data); 61 | }); 62 | } 63 | 64 | function writeEvent(res, id, data) { 65 | res.write('id: ' + id + '\n'); 66 | res.write('data: ' + data + '\n\n'); 67 | res.flush(); 68 | } 69 | 70 | function removeConnection(res) { 71 | var i = connections.indexOf(res); 72 | if (i !== -1) { 73 | connections.splice(i, 1); 74 | console.log('Connection closed.'); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /slurp/src/main/java/org/json/HTTPTokener.java: -------------------------------------------------------------------------------- 1 | package org.json; 2 | 3 | /* 4 | Copyright (c) 2002 JSON.org 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | The Software shall be used for Good, not Evil. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | */ 26 | 27 | /** 28 | * The HTTPTokener extends the JSONTokener to provide additional methods 29 | * for the parsing of HTTP headers. 30 | * @author JSON.org 31 | * @version 2012-11-13 32 | */ 33 | public class HTTPTokener extends JSONTokener { 34 | 35 | /** 36 | * Construct an HTTPTokener from a string. 37 | * @param string A source string. 38 | */ 39 | public HTTPTokener(String string) { 40 | super(string); 41 | } 42 | 43 | 44 | /** 45 | * Get the next token or string. This is used in parsing HTTP headers. 46 | * @throws JSONException 47 | * @return A String. 48 | */ 49 | public String nextToken() throws JSONException { 50 | char c; 51 | char q; 52 | StringBuffer sb = new StringBuffer(); 53 | do { 54 | c = next(); 55 | } while (Character.isWhitespace(c)); 56 | if (c == '"' || c == '\'') { 57 | q = c; 58 | for (;;) { 59 | c = next(); 60 | if (c < ' ') { 61 | throw syntaxError("Unterminated string."); 62 | } 63 | if (c == q) { 64 | return sb.toString(); 65 | } 66 | sb.append(c); 67 | } 68 | } 69 | for (;;) { 70 | if (c == 0 || Character.isWhitespace(c)) { 71 | return sb.toString(); 72 | } 73 | sb.append(c); 74 | c = next(); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /cartographer/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Git Live 6 | 7 | 8 | 9 | 18 | 19 | 20 |
21 | 41 | 42 | 45 |
46 |
47 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /slurp/src/main/java/org/json/Property.java: -------------------------------------------------------------------------------- 1 | package org.json; 2 | 3 | /* 4 | Copyright (c) 2002 JSON.org 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | The Software shall be used for Good, not Evil. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | */ 26 | 27 | import java.util.Enumeration; 28 | import java.util.Iterator; 29 | import java.util.Properties; 30 | 31 | /** 32 | * Converts a Property file data into JSONObject and back. 33 | * @author JSON.org 34 | * @version 2013-05-23 35 | */ 36 | public class Property { 37 | /** 38 | * Converts a property file object into a JSONObject. The property file object is a table of name value pairs. 39 | * @param properties java.util.Properties 40 | * @return JSONObject 41 | * @throws JSONException 42 | */ 43 | public static JSONObject toJSONObject(java.util.Properties properties) throws JSONException { 44 | JSONObject jo = new JSONObject(); 45 | if (properties != null && !properties.isEmpty()) { 46 | Enumeration enumProperties = properties.propertyNames(); 47 | while(enumProperties.hasMoreElements()) { 48 | String name = (String)enumProperties.nextElement(); 49 | jo.put(name, properties.getProperty(name)); 50 | } 51 | } 52 | return jo; 53 | 54 | } 55 | 56 | /** 57 | * Converts the JSONObject into a property file object. 58 | * @param jo JSONObject 59 | * @return java.util.Properties 60 | * @throws JSONException 61 | */ 62 | public static Properties toProperties(JSONObject jo) throws JSONException { 63 | Properties properties = new Properties(); 64 | if (jo != null) { 65 | Iterator keys = jo.keys(); 66 | 67 | while (keys.hasNext()) { 68 | String name = keys.next().toString(); 69 | properties.put(name, jo.getString(name)); 70 | } 71 | } 72 | return properties; 73 | } 74 | } -------------------------------------------------------------------------------- /slurp/src/main/java/org/json/JSONStringer.java: -------------------------------------------------------------------------------- 1 | package org.json; 2 | 3 | /* 4 | Copyright (c) 2006 JSON.org 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | The Software shall be used for Good, not Evil. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | */ 26 | 27 | import java.io.StringWriter; 28 | 29 | /** 30 | * JSONStringer provides a quick and convenient way of producing JSON text. 31 | * The texts produced strictly conform to JSON syntax rules. No whitespace is 32 | * added, so the results are ready for transmission or storage. Each instance of 33 | * JSONStringer can produce one JSON text. 34 | *

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

43 |  * myString = new JSONStringer()
44 |  *     .object()
45 |  *         .key("JSON")
46 |  *         .value("Hello, World!")
47 |  *     .endObject()
48 |  *     .toString();
which produces the string
49 |  * {"JSON":"Hello, World!"}
50 | *

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

55 | * This can sometimes be easier than using a JSONObject to build a string. 56 | * @author JSON.org 57 | * @version 2008-09-18 58 | */ 59 | public class JSONStringer extends JSONWriter { 60 | /** 61 | * Make a fresh JSONStringer. It can be used to build one JSON text. 62 | */ 63 | public JSONStringer() { 64 | super(new StringWriter()); 65 | } 66 | 67 | /** 68 | * Return the JSON text. This method is used to obtain the product of the 69 | * JSONStringer instance. It will return null if there was a 70 | * problem in the construction of the JSON text (such as the calls to 71 | * array were not properly balanced with calls to 72 | * endArray). 73 | * @return The JSON text. 74 | */ 75 | public String toString() { 76 | return this.mode == 'd' ? this.writer.toString() : null; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /slurp/src/main/java/org/json/CookieList.java: -------------------------------------------------------------------------------- 1 | package org.json; 2 | 3 | /* 4 | Copyright (c) 2002 JSON.org 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | The Software shall be used for Good, not Evil. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | */ 26 | 27 | import java.util.Iterator; 28 | 29 | /** 30 | * Convert a web browser cookie list string to a JSONObject and back. 31 | * @author JSON.org 32 | * @version 2010-12-24 33 | */ 34 | public class CookieList { 35 | 36 | /** 37 | * Convert a cookie list into a JSONObject. A cookie list is a sequence 38 | * of name/value pairs. The names are separated from the values by '='. 39 | * The pairs are separated by ';'. The names and the values 40 | * will be unescaped, possibly converting '+' and '%' sequences. 41 | * 42 | * To add a cookie to a cooklist, 43 | * cookielistJSONObject.put(cookieJSONObject.getString("name"), 44 | * cookieJSONObject.getString("value")); 45 | * @param string A cookie list string 46 | * @return A JSONObject 47 | * @throws JSONException 48 | */ 49 | public static JSONObject toJSONObject(String string) throws JSONException { 50 | JSONObject jo = new JSONObject(); 51 | JSONTokener x = new JSONTokener(string); 52 | while (x.more()) { 53 | String name = Cookie.unescape(x.nextTo('=')); 54 | x.next('='); 55 | jo.put(name, Cookie.unescape(x.nextTo(';'))); 56 | x.next(); 57 | } 58 | return jo; 59 | } 60 | 61 | 62 | /** 63 | * Convert a JSONObject into a cookie list. A cookie list is a sequence 64 | * of name/value pairs. The names are separated from the values by '='. 65 | * The pairs are separated by ';'. The characters '%', '+', '=', and ';' 66 | * in the names and values are replaced by "%hh". 67 | * @param jo A JSONObject 68 | * @return A cookie list string 69 | * @throws JSONException 70 | */ 71 | public static String toString(JSONObject jo) throws JSONException { 72 | boolean b = false; 73 | Iterator keys = jo.keys(); 74 | String string; 75 | StringBuffer sb = new StringBuffer(); 76 | while (keys.hasNext()) { 77 | string = keys.next().toString(); 78 | if (!jo.isNull(string)) { 79 | if (b) { 80 | sb.append(';'); 81 | } 82 | sb.append(Cookie.escape(string)); 83 | sb.append("="); 84 | sb.append(Cookie.escape(jo.getString(string))); 85 | b = true; 86 | } 87 | } 88 | return sb.toString(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /slurp/src/main/scala/morph/parser/JsonParser.scala: -------------------------------------------------------------------------------- 1 | package morph.parser 2 | 3 | import morph.ast._ 4 | 5 | import org.parboiled.scala._ 6 | import org.parboiled.Context 7 | 8 | import scala.collection.mutable.Builder 9 | import java.lang.StringBuilder 10 | 11 | /** 12 | * A JSON parser that constructs an AST. 13 | * 14 | * It can parse JSON files that conform to the specification available 15 | * at www.json.org. This implimentation almost directly follows the 16 | * grammar specified there. 17 | * 18 | * @author Anish Athalye 19 | */ 20 | object JsonParser extends BaseParser with WhiteSpaceExpansion { 21 | 22 | def RootRule = Json 23 | 24 | lazy val Json = rule { WhiteSpace ~ Value ~ EOI } 25 | 26 | def JsonObject = rule { 27 | "{ " ~ JsonObjectUnwrapped ~ "} " ~~> { ObjectNode(_) } 28 | } 29 | 30 | def JsonObjectUnwrapped = rule { Pairs ~~> { _.result } } 31 | 32 | def Pairs = rule { 33 | push(Map.newBuilder[String, ValueNode]) ~ zeroOrMore(rule { 34 | Pair ~~% { withContext[String, ValueNode, Unit](appendToMb) } 35 | }, separator = ", ") 36 | } 37 | 38 | def Pair = rule { JsonStringUnwrapped ~ ": " ~ Value } 39 | 40 | def Value: Rule1[ValueNode] = rule { 41 | JsonString | JsonNumber | JsonObject | JsonArray | 42 | JsonTrue | JsonFalse | JsonNull 43 | } 44 | 45 | def JsonString = rule { JsonStringUnwrapped ~~> { StringNode(_) } } 46 | 47 | def JsonStringUnwrapped = rule { 48 | "\"" ~ Characters ~ "\" " ~~> { _.toString } 49 | } 50 | 51 | def JsonNumber = rule { 52 | group(Integer ~ optional(Frac) ~ optional(Exp)) ~> 53 | { NumberNode(_) } ~ WhiteSpace 54 | } 55 | 56 | def JsonArray = rule { 57 | "[ " ~ JsonArrayUnwrapped ~ "] " ~~> { ArrayNode(_) } 58 | } 59 | 60 | def JsonArrayUnwrapped = rule { Values ~~> { _.result } } 61 | 62 | def Values = rule { 63 | push(Vector.newBuilder[ValueNode]) ~ zeroOrMore(rule { 64 | Value ~~% { withContext(appendToVb(_: ValueNode, _)) } 65 | }, separator = ", ") 66 | } 67 | 68 | def Characters = rule { 69 | push(new StringBuilder) ~ zeroOrMore("\\" ~ EscapedChar | NormalChar) 70 | } 71 | 72 | def EscapedChar = { 73 | def unicode(code: Int, ctx: Context[_]) { 74 | appendToSb(code.asInstanceOf[Char], ctx) 75 | } 76 | def escaped(c: Char, ctx: Context[_]) { 77 | appendToSb('\\', ctx) 78 | appendToSb(c, ctx) 79 | } 80 | rule { 81 | anyOf("\"\\/") ~:% withContext(appendToSb) | 82 | anyOf("bfnrt") ~:% withContext(escaped) | 83 | Unicode ~~% { 84 | withContext(unicode) 85 | } 86 | } 87 | } 88 | 89 | def NormalChar = rule { 90 | !anyOf("\"\\") ~ ANY ~:% { withContext(appendToSb) } 91 | } 92 | 93 | def Unicode = rule { 94 | "u" ~ group(HexDigit ~ HexDigit ~ HexDigit ~ HexDigit) ~> 95 | { java.lang.Integer.parseInt(_, 16) } 96 | } 97 | 98 | def Integer = rule { optional("-") ~ (("1" - "9") ~ Digits | Digit) } 99 | 100 | def Digits = rule { oneOrMore(Digit) } 101 | 102 | def Digit = rule { "0" - "9" } 103 | 104 | def HexDigit = rule { Digit | "a" - "f" | "A" - "F" } 105 | 106 | def Frac = rule { "." ~ Digits } 107 | 108 | def Exp = rule { ignoreCase("e") ~ optional(anyOf("+-")) ~ Digits } 109 | 110 | def JsonTrue = rule { "true " ~ push(TrueNode) } 111 | 112 | def JsonFalse = rule { "false " ~ push(FalseNode) } 113 | 114 | def JsonNull = rule { "null " ~ push(NullNode) } 115 | 116 | def appendToSb(c: Char, ctx: Context[_]) { 117 | ctx.getValueStack.peek.asInstanceOf[StringBuilder].append(c) 118 | } 119 | 120 | def appendToMb(k: String, v: ValueNode, ctx: Context[_]) { 121 | ctx.getValueStack.peek.asInstanceOf[Builder[(String, ValueNode), Map[String, ValueNode]]] += ((k, v)) 122 | } 123 | 124 | def appendToVb[T](e: T, ctx: Context[_]) { 125 | ctx.getValueStack.peek.asInstanceOf[Builder[T, Vector[T]]] += e 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /slurp/src/main/scala/morph/ast/Ast.scala: -------------------------------------------------------------------------------- 1 | package morph.ast 2 | 3 | import morph.utils.Utils._ 4 | 5 | import collection.immutable.ListMap 6 | import scala.language.implicitConversions 7 | 8 | /** 9 | * The abstract superclass for all AST node types. 10 | * 11 | * @author Anish Athalye 12 | */ 13 | sealed abstract class ValueNode 14 | 15 | /** 16 | * An object. 17 | * 18 | * @author Anish Athalye 19 | */ 20 | case class ObjectNode(fields: Map[String, ValueNode]) extends ValueNode { 21 | 22 | override def toString = { 23 | val mapStr = fields map { 24 | case (k, v) => "\"" + k + "\": " + v 25 | } mkString ",\n" 26 | if (fields.isEmpty) { 27 | "{}" 28 | } else { 29 | "{\n" + mapStr.indent + "\n}" 30 | } 31 | } 32 | } 33 | 34 | object ObjectNode { 35 | 36 | def apply(fields: (String, ValueNode)*) = 37 | new ObjectNode(ListMap(fields: _*)) 38 | 39 | def apply(fields: List[(String, ValueNode)]) = 40 | new ObjectNode(ListMap(fields: _*)) 41 | } 42 | 43 | /** 44 | * An array. 45 | * 46 | * @author Anish Athalye 47 | */ 48 | case class ArrayNode(elements: IndexedSeq[ValueNode]) extends ValueNode { 49 | 50 | override def toString = if (elements.isEmpty) { 51 | "[]" 52 | } else { 53 | "[\n" + elements.mkString(",\n").indent + "\n]" 54 | } 55 | } 56 | 57 | object ArrayNode { 58 | 59 | def apply(elements: List[ValueNode]) = new ArrayNode(elements.toVector) 60 | 61 | def apply(elements: ValueNode*) = new ArrayNode(elements.toVector) 62 | } 63 | 64 | /** 65 | * A string. 66 | * 67 | * @author Anish Athalye 68 | */ 69 | case class StringNode(value: String) extends ValueNode { 70 | 71 | override def toString = "\"" + value.escaped + "\"" 72 | } 73 | 74 | object StringNode { 75 | 76 | def apply(sym: Symbol) = new StringNode(sym.name) 77 | } 78 | 79 | /** 80 | * A number. 81 | * 82 | * A generic number type internally represented as a BigDecimal. 83 | * 84 | * @author Anish Athalye 85 | */ 86 | case class NumberNode(value: BigDecimal) extends ValueNode { 87 | 88 | override def toString = value.toString 89 | } 90 | 91 | object NumberNode { 92 | 93 | def apply(n: Int) = new NumberNode(BigDecimal(n)) 94 | 95 | def apply(n: Long) = new NumberNode(BigDecimal(n)) 96 | 97 | def apply(n: Double) = n match { 98 | case n if n.isNaN => NullNode 99 | case n if n.isInfinity => NullNode 100 | case _ => new NumberNode(BigDecimal(n)) 101 | } 102 | 103 | def apply(n: BigInt) = new NumberNode(BigDecimal(n)) 104 | 105 | def apply(n: String) = new NumberNode(BigDecimal(n)) 106 | } 107 | 108 | sealed abstract class BooleanNode extends ValueNode { 109 | 110 | def value: Boolean 111 | 112 | override def toString = value.toString 113 | } 114 | 115 | /** 116 | * A boolean. 117 | * 118 | * @author Anish Athalye 119 | */ 120 | object BooleanNode { 121 | 122 | def apply(x: Boolean): BooleanNode = 123 | if (x) TrueNode else FalseNode 124 | 125 | def unapply(x: BooleanNode): Option[Boolean] = Some(x.value) 126 | } 127 | 128 | case object TrueNode extends BooleanNode { 129 | 130 | def value = true 131 | } 132 | 133 | case object FalseNode extends BooleanNode { 134 | 135 | def value = false 136 | } 137 | 138 | /** 139 | * A null value. 140 | * 141 | * @author Anish Athalye 142 | */ 143 | case object NullNode extends ValueNode { 144 | 145 | override def toString = "null" 146 | } 147 | 148 | /** 149 | * Implicit conversions from several scala built in data types to 150 | * their corresponding AST representations. 151 | * 152 | * @author Anish Athalye 153 | */ 154 | trait Implicits { 155 | 156 | implicit def String2StringNode(s: String): StringNode = StringNode(s) 157 | 158 | implicit def Boolean2BooleanNode(b: Boolean): BooleanNode = BooleanNode(b) 159 | 160 | implicit def Int2NumberNode(n: Int): NumberNode = NumberNode(n) 161 | 162 | implicit def Long2NumberNode(n: Long): NumberNode = NumberNode(n) 163 | 164 | implicit def Double2NumberNode(n: Double): NumberNode = NumberNode(n) 165 | 166 | implicit def BigDecimal2NumberNode(n: BigDecimal): NumberNode = NumberNode(n) 167 | 168 | implicit def StringValueNodeViewable2StringValueNode[T <% ValueNode]( 169 | ss: (String, T)): (String, ValueNode) = (ss._1, ss._2) 170 | } 171 | 172 | /** 173 | * A companion object to make it possible to either 174 | * mix in the trait or import the companion object's methods. 175 | */ 176 | object Implicits extends Implicits 177 | -------------------------------------------------------------------------------- /slurp/src/main/scala/slurp/GitHubStream.scala: -------------------------------------------------------------------------------- 1 | package slurp 2 | 3 | import scala.async.Async.{ async, await } 4 | import scala.concurrent.Await 5 | import scala.concurrent.duration._ 6 | 7 | import java.util.concurrent.{ ExecutionException, TimeoutException } 8 | 9 | import dispatch._, Defaults._ 10 | 11 | import com.twitter.util.LruMap 12 | 13 | import morph.ast._, DSL._, Implicits._ 14 | import morph.parser._ 15 | 16 | final class GitHubStream(clientId: String, clientSecret: String) { 17 | 18 | private var lastPollMillis: Long = 0 19 | private var pollIntervalMillis: Long = 0 20 | private var eTag: String = "" 21 | private var lastId = BigInt("-1") 22 | 23 | def getEvents(): (List[String], Long) = { 24 | val (events, pollInterval) = getRawEvents() 25 | val filtered = events filter { event => 26 | val fln = event lift "fromLocation" match { 27 | case Some(loc) => loc != "" 28 | case None => false 29 | } 30 | val tln = event lift "toLocation" match { 31 | case Some(loc) => loc != "" 32 | case None => false 33 | } 34 | fln && tln 35 | } map { event => 36 | ObjectNode(event mapValues { value => 37 | StringNode(value) 38 | }).toString 39 | } 40 | (filtered, pollInterval) 41 | } 42 | 43 | def getRawEvents(): (List[Map[String, String]], Long) = { 44 | try { 45 | val headers = Map("If-None-Match" -> eTag) 46 | val url = apiBase / "events" < 0) { 51 | Thread.sleep(sleepTimeMillis) 52 | } 53 | 54 | val resp = Await.result(req, DEFAULT_TIMEOUT) 55 | 56 | lastPollMillis = System.currentTimeMillis 57 | val pollInterval = (resp getHeader "X-Poll-Interval").toLong / 16 // speed up 58 | pollIntervalMillis = pollInterval * 1000 59 | eTag = resp getHeader "ETag" // update for next request 60 | 61 | val json = JsonParser(resp.getResponseBody).asList 62 | val nextId = if (json.nonEmpty) { 63 | BigInt((json.head ~> "id").asString) 64 | } else { 65 | lastId 66 | } 67 | val latest = json filter { event => 68 | BigInt((event ~> "id").asString) > lastId 69 | } 70 | lastId = nextId 71 | 72 | val filtered = latest filter { event => 73 | (event ~> "type").asString match { 74 | case "ForkEvent" => true 75 | case "WatchEvent" => true 76 | case "PullRequestEvent" => (event ~> "payload" ~> "action").asString == "opened" 77 | case "IssuesEvent" => (event ~> "payload" ~> "action").asString == "opened" 78 | case _ => false 79 | } 80 | } 81 | 82 | val events = filtered map { event => 83 | val eventType = (event ~> "type").asString 84 | val login = (event ~> "actor" ~> "login").asString 85 | val (targetLogin, targetRest) = (event ~> "repo" ~> "name").asString span { _ != '/' } 86 | val targetRepo = targetRest.tail 87 | val url = (event ~> "type").asString match { 88 | case "ForkEvent" => (event ~> "payload" ~> "forkee" ~> "html_url").asString 89 | case "WatchEvent" => s"https://github.com/${(event ~> "repo" ~> "name").asString}" 90 | case "PullRequestEvent" => (event ~> "payload" ~> "pull_request" ~> "html_url").asString 91 | case "IssuesEvent" => (event ~> "payload" ~> "issue" ~> "html_url").asString 92 | case _ => "https://github.com/" 93 | } 94 | async { 95 | val fromLocation = await(getUserLocation(login)) 96 | val toLocation = await(getUserLocation(targetLogin)) 97 | Map( 98 | "type" -> eventType, 99 | "fromLogin" -> login, 100 | "fromLocation" -> fromLocation, 101 | "toRepo" -> targetRepo, 102 | "toLogin" -> targetLogin, 103 | "toLocation" -> toLocation, 104 | "url" -> url 105 | ) 106 | } 107 | } 108 | (Await.result(Future.sequence(events), pollInterval.seconds), pollInterval) 109 | } catch { 110 | case e @ (_: ExecutionException | _: TimeoutException) => { 111 | e.printStackTrace() 112 | (List(Map()), 0) 113 | } 114 | } 115 | } 116 | 117 | private val LRU_MAP_SIZE = 20000 118 | private val locations: LruMap[String, String] = new LruMap[String, String](LRU_MAP_SIZE) 119 | 120 | def getUserLocation(user: String): Future[String] = { 121 | val location = locations.synchronized { locations lift user } 122 | location match { 123 | case Some(loc) => Future(loc) 124 | case None => { 125 | val url = apiBase / "users" / user < 128 | val loc = JsonParser(res) ~> "location" collect { 129 | case StringNode(l) => l 130 | } getOrElse "" 131 | locations.synchronized { 132 | locations(user) = loc 133 | } 134 | loc 135 | } 136 | } 137 | } 138 | } 139 | 140 | private val apiBase = url("https://api.github.com") 141 | private val keys = Map("client_id" -> clientId, "client_secret" -> clientSecret) 142 | private val userAgent = Map("User-Agent" -> "anishathalye") 143 | 144 | private val DEFAULT_TIMEOUT = 10.seconds 145 | 146 | } 147 | -------------------------------------------------------------------------------- /slurp/src/main/java/org/json/HTTP.java: -------------------------------------------------------------------------------- 1 | package org.json; 2 | 3 | /* 4 | Copyright (c) 2002 JSON.org 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | The Software shall be used for Good, not Evil. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | */ 26 | 27 | import java.util.Iterator; 28 | 29 | /** 30 | * Convert an HTTP header to a JSONObject and back. 31 | * @author JSON.org 32 | * @version 2010-12-24 33 | */ 34 | public class HTTP { 35 | 36 | /** Carriage return/line feed. */ 37 | public static final String CRLF = "\r\n"; 38 | 39 | /** 40 | * Convert an HTTP header string into a JSONObject. It can be a request 41 | * header or a response header. A request header will contain 42 | *

{
 43 |      *    Method: "POST" (for example),
 44 |      *    "Request-URI": "/" (for example),
 45 |      *    "HTTP-Version": "HTTP/1.1" (for example)
 46 |      * }
47 | * A response header will contain 48 | *
{
 49 |      *    "HTTP-Version": "HTTP/1.1" (for example),
 50 |      *    "Status-Code": "200" (for example),
 51 |      *    "Reason-Phrase": "OK" (for example)
 52 |      * }
53 | * In addition, the other parameters in the header will be captured, using 54 | * the HTTP field names as JSON names, so that
 55 |      *    Date: Sun, 26 May 2002 18:06:04 GMT
 56 |      *    Cookie: Q=q2=PPEAsg--; B=677gi6ouf29bn&b=2&f=s
 57 |      *    Cache-Control: no-cache
58 | * become 59 | *
{...
 60 |      *    Date: "Sun, 26 May 2002 18:06:04 GMT",
 61 |      *    Cookie: "Q=q2=PPEAsg--; B=677gi6ouf29bn&b=2&f=s",
 62 |      *    "Cache-Control": "no-cache",
 63 |      * ...}
64 | * It does no further checking or conversion. It does not parse dates. 65 | * It does not do '%' transforms on URLs. 66 | * @param string An HTTP header string. 67 | * @return A JSONObject containing the elements and attributes 68 | * of the XML string. 69 | * @throws JSONException 70 | */ 71 | public static JSONObject toJSONObject(String string) throws JSONException { 72 | JSONObject jo = new JSONObject(); 73 | HTTPTokener x = new HTTPTokener(string); 74 | String token; 75 | 76 | token = x.nextToken(); 77 | if (token.toUpperCase().startsWith("HTTP")) { 78 | 79 | // Response 80 | 81 | jo.put("HTTP-Version", token); 82 | jo.put("Status-Code", x.nextToken()); 83 | jo.put("Reason-Phrase", x.nextTo('\0')); 84 | x.next(); 85 | 86 | } else { 87 | 88 | // Request 89 | 90 | jo.put("Method", token); 91 | jo.put("Request-URI", x.nextToken()); 92 | jo.put("HTTP-Version", x.nextToken()); 93 | } 94 | 95 | // Fields 96 | 97 | while (x.more()) { 98 | String name = x.nextTo(':'); 99 | x.next(':'); 100 | jo.put(name, x.nextTo('\0')); 101 | x.next(); 102 | } 103 | return jo; 104 | } 105 | 106 | 107 | /** 108 | * Convert a JSONObject into an HTTP header. A request header must contain 109 | *
{
110 |      *    Method: "POST" (for example),
111 |      *    "Request-URI": "/" (for example),
112 |      *    "HTTP-Version": "HTTP/1.1" (for example)
113 |      * }
114 | * A response header must contain 115 | *
{
116 |      *    "HTTP-Version": "HTTP/1.1" (for example),
117 |      *    "Status-Code": "200" (for example),
118 |      *    "Reason-Phrase": "OK" (for example)
119 |      * }
120 | * Any other members of the JSONObject will be output as HTTP fields. 121 | * The result will end with two CRLF pairs. 122 | * @param jo A JSONObject 123 | * @return An HTTP header string. 124 | * @throws JSONException if the object does not contain enough 125 | * information. 126 | */ 127 | public static String toString(JSONObject jo) throws JSONException { 128 | Iterator keys = jo.keys(); 129 | String string; 130 | StringBuffer sb = new StringBuffer(); 131 | if (jo.has("Status-Code") && jo.has("Reason-Phrase")) { 132 | sb.append(jo.getString("HTTP-Version")); 133 | sb.append(' '); 134 | sb.append(jo.getString("Status-Code")); 135 | sb.append(' '); 136 | sb.append(jo.getString("Reason-Phrase")); 137 | } else if (jo.has("Method") && jo.has("Request-URI")) { 138 | sb.append(jo.getString("Method")); 139 | sb.append(' '); 140 | sb.append('"'); 141 | sb.append(jo.getString("Request-URI")); 142 | sb.append('"'); 143 | sb.append(' '); 144 | sb.append(jo.getString("HTTP-Version")); 145 | } else { 146 | throw new JSONException("Not enough material for an HTTP header."); 147 | } 148 | sb.append(CRLF); 149 | while (keys.hasNext()) { 150 | string = keys.next().toString(); 151 | if (!"HTTP-Version".equals(string) && !"Status-Code".equals(string) && 152 | !"Reason-Phrase".equals(string) && !"Method".equals(string) && 153 | !"Request-URI".equals(string) && !jo.isNull(string)) { 154 | sb.append(string); 155 | sb.append(": "); 156 | sb.append(jo.getString(string)); 157 | sb.append(CRLF); 158 | } 159 | } 160 | sb.append(CRLF); 161 | return sb.toString(); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /slurp/src/main/java/org/json/Cookie.java: -------------------------------------------------------------------------------- 1 | package org.json; 2 | 3 | /* 4 | Copyright (c) 2002 JSON.org 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | The Software shall be used for Good, not Evil. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | */ 26 | 27 | /** 28 | * Convert a web browser cookie specification to a JSONObject and back. 29 | * JSON and Cookies are both notations for name/value pairs. 30 | * @author JSON.org 31 | * @version 2010-12-24 32 | */ 33 | public class Cookie { 34 | 35 | /** 36 | * Produce a copy of a string in which the characters '+', '%', '=', ';' 37 | * and control characters are replaced with "%hh". This is a gentle form 38 | * of URL encoding, attempting to cause as little distortion to the 39 | * string as possible. The characters '=' and ';' are meta characters in 40 | * cookies. By convention, they are escaped using the URL-encoding. This is 41 | * only a convention, not a standard. Often, cookies are expected to have 42 | * encoded values. We encode '=' and ';' because we must. We encode '%' and 43 | * '+' because they are meta characters in URL encoding. 44 | * @param string The source string. 45 | * @return The escaped result. 46 | */ 47 | public static String escape(String string) { 48 | char c; 49 | String s = string.trim(); 50 | StringBuffer sb = new StringBuffer(); 51 | int length = s.length(); 52 | for (int i = 0; i < length; i += 1) { 53 | c = s.charAt(i); 54 | if (c < ' ' || c == '+' || c == '%' || c == '=' || c == ';') { 55 | sb.append('%'); 56 | sb.append(Character.forDigit((char)((c >>> 4) & 0x0f), 16)); 57 | sb.append(Character.forDigit((char)(c & 0x0f), 16)); 58 | } else { 59 | sb.append(c); 60 | } 61 | } 62 | return sb.toString(); 63 | } 64 | 65 | 66 | /** 67 | * Convert a cookie specification string into a JSONObject. The string 68 | * will contain a name value pair separated by '='. The name and the value 69 | * will be unescaped, possibly converting '+' and '%' sequences. The 70 | * cookie properties may follow, separated by ';', also represented as 71 | * name=value (except the secure property, which does not have a value). 72 | * The name will be stored under the key "name", and the value will be 73 | * stored under the key "value". This method does not do checking or 74 | * validation of the parameters. It only converts the cookie string into 75 | * a JSONObject. 76 | * @param string The cookie specification string. 77 | * @return A JSONObject containing "name", "value", and possibly other 78 | * members. 79 | * @throws JSONException 80 | */ 81 | public static JSONObject toJSONObject(String string) throws JSONException { 82 | String name; 83 | JSONObject jo = new JSONObject(); 84 | Object value; 85 | JSONTokener x = new JSONTokener(string); 86 | jo.put("name", x.nextTo('=')); 87 | x.next('='); 88 | jo.put("value", x.nextTo(';')); 89 | x.next(); 90 | while (x.more()) { 91 | name = unescape(x.nextTo("=;")); 92 | if (x.next() != '=') { 93 | if (name.equals("secure")) { 94 | value = Boolean.TRUE; 95 | } else { 96 | throw x.syntaxError("Missing '=' in cookie parameter."); 97 | } 98 | } else { 99 | value = unescape(x.nextTo(';')); 100 | x.next(); 101 | } 102 | jo.put(name, value); 103 | } 104 | return jo; 105 | } 106 | 107 | 108 | /** 109 | * Convert a JSONObject into a cookie specification string. The JSONObject 110 | * must contain "name" and "value" members. 111 | * If the JSONObject contains "expires", "domain", "path", or "secure" 112 | * members, they will be appended to the cookie specification string. 113 | * All other members are ignored. 114 | * @param jo A JSONObject 115 | * @return A cookie specification string 116 | * @throws JSONException 117 | */ 118 | public static String toString(JSONObject jo) throws JSONException { 119 | StringBuffer sb = new StringBuffer(); 120 | 121 | sb.append(escape(jo.getString("name"))); 122 | sb.append("="); 123 | sb.append(escape(jo.getString("value"))); 124 | if (jo.has("expires")) { 125 | sb.append(";expires="); 126 | sb.append(jo.getString("expires")); 127 | } 128 | if (jo.has("domain")) { 129 | sb.append(";domain="); 130 | sb.append(escape(jo.getString("domain"))); 131 | } 132 | if (jo.has("path")) { 133 | sb.append(";path="); 134 | sb.append(escape(jo.getString("path"))); 135 | } 136 | if (jo.optBoolean("secure")) { 137 | sb.append(";secure"); 138 | } 139 | return sb.toString(); 140 | } 141 | 142 | /** 143 | * Convert %hh sequences to single characters, and 144 | * convert plus to space. 145 | * @param string A string that may contain 146 | * + (plus) and 147 | * %hh sequences. 148 | * @return The unescaped string. 149 | */ 150 | public static String unescape(String string) { 151 | int length = string.length(); 152 | StringBuffer sb = new StringBuffer(); 153 | for (int i = 0; i < length; ++i) { 154 | char c = string.charAt(i); 155 | if (c == '+') { 156 | c = ' '; 157 | } else if (c == '%' && i + 2 < length) { 158 | int d = JSONTokener.dehexchar(string.charAt(i + 1)); 159 | int e = JSONTokener.dehexchar(string.charAt(i + 2)); 160 | if (d >= 0 && e >= 0) { 161 | c = (char)(d * 16 + e); 162 | i += 2; 163 | } 164 | } 165 | sb.append(c); 166 | } 167 | return sb.toString(); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /cartographer/public/css/gitlive.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | overflow: hidden; 3 | height: 100%; 4 | width: 100%; 5 | margin: 0; 6 | padding: 0; 7 | background: #22262e; 8 | font-family: "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif; 9 | } 10 | 11 | #footer { 12 | height: 50px; 13 | width: 100%; 14 | position: fixed; 15 | bottom: 5px; 16 | } 17 | 18 | #footer span { 19 | position: absolute; 20 | bottom: 0; 21 | width: 100%; 22 | text-align: center; 23 | font-weight: 300; 24 | font-size: 10px; 25 | color: #424a59; 26 | } 27 | 28 | #footer a { 29 | color: #4b5466; 30 | text-decoration: none; 31 | -webkit-transition: all 0.15s linear; 32 | -moz-transition: all 0.15s linear; 33 | -o-transition: all 0.15s linear; 34 | transition: all 0.15s linear; 35 | } 36 | 37 | #footer a:hover { 38 | color: rgba(255, 255, 255, 0.5); 39 | text-shadow: rgba(255, 255, 255, 0.5) 0 0 3px; 40 | } 41 | 42 | #map { 43 | height: 100%; 44 | width: 100%; 45 | margin: 0 auto; 46 | } 47 | 48 | .shade { 49 | background: rgba(0,0,0,.7); 50 | } 51 | 52 | .lightbox-target { 53 | position: fixed; 54 | top: -100%; 55 | width: 100%; 56 | opacity: 0; 57 | -webkit-transition: opacity .5s ease-in-out; 58 | -moz-transition: opacity .5s ease-in-out; 59 | -o-transition: opacity .5s ease-in-out; 60 | transition: opacity .5s ease-in-out; 61 | overflow: hidden; 62 | } 63 | 64 | :target div.lightbox-target { 65 | opacity: 1; 66 | top: 0; 67 | bottom: 0; 68 | } 69 | 70 | .lightbox-close-target { 71 | position: fixed; 72 | top: -100%; 73 | width: 50px; 74 | height: 50px; 75 | right: 0; 76 | overflow: hidden; 77 | } 78 | 79 | :target div.lightbox-close-target { 80 | top: 0; 81 | } 82 | 83 | div.lightbox-content { 84 | margin: auto; 85 | overflow-y: auto; 86 | padding: 15px 15px 15px 15px; 87 | position: absolute; 88 | top: 0; 89 | left: 0; 90 | right: 0; 91 | bottom: 0; 92 | height: 100%; 93 | width: 100%; 94 | background: #1a1c23; 95 | border-radius: 5px; 96 | color: #8d9ebf; 97 | } 98 | 99 | @media (min-width: 900px) { 100 | div.lightbox-content { 101 | height: 75%; 102 | width: 768px; 103 | } 104 | } 105 | 106 | .lightbox-content h1 { 107 | text-align: center; 108 | } 109 | 110 | .lightbox-content hr { 111 | border: 1px solid #8d9ebf; 112 | width: 50%; 113 | } 114 | 115 | .lightbox-content img { 116 | margin: auto; 117 | display: block; 118 | max-width: 50%; 119 | border: 1px solid #8d9ebf; 120 | } 121 | 122 | .lightbox-content a { 123 | color: #bcd3ff; 124 | text-decoration: none; 125 | -webkit-transition: all 0.15s linear; 126 | -moz-transition: all 0.15s linear; 127 | -o-transition: all 0.15s linear; 128 | transition: all 0.15s linear; 129 | } 130 | 131 | .lightbox-content a:hover { 132 | text-shadow: #bcd3ff 0 0 3px; 133 | } 134 | 135 | .lightbox-content span.red { 136 | color: #ea5b89; 137 | } 138 | 139 | .lightbox-content span.green { 140 | color: #4fcb7d; 141 | } 142 | 143 | .lightbox-content span.blue { 144 | color: #46b3b3; 145 | } 146 | 147 | .lightbox-content span.yellow { 148 | color: #ffd564; 149 | } 150 | 151 | a.lightbox-close { 152 | display: block; 153 | width:50px; 154 | height:50px; 155 | box-sizing: border-box; 156 | background: transparent; 157 | color: black; 158 | text-decoration: none; 159 | position: absolute; 160 | top: 0px; 161 | right: 0; 162 | } 163 | 164 | a.plus { 165 | display: block; 166 | width:50px; 167 | height:50px; 168 | box-sizing: border-box; 169 | background: #555f73; 170 | color: #8d9ebf; 171 | text-decoration: none; 172 | position: absolute; 173 | top: 0px; 174 | right: 0; 175 | -webkit-transition: transform .5s ease-in-out; 176 | -moz-transition: transform .5s ease-in-out; 177 | -o-transition: transform .5s ease-in-out; 178 | transition: transform .5s ease-in-out; 179 | -webkit-transition: background-color .5s ease-in-out; 180 | -moz-transition: background-color .5s ease-in-out; 181 | -o-transition: background-color .5s ease-in-out; 182 | transition: background-color .5s ease-in-out; 183 | } 184 | 185 | :target a.plus { 186 | background: transparent; 187 | } 188 | 189 | a.plus:before { 190 | content: ""; 191 | display: block; 192 | height: 30px; 193 | width: 1px; 194 | background: #8d9ebf; 195 | position: absolute; 196 | left: 26px; 197 | top:10px; 198 | -webkit-transform:rotate(0deg); 199 | -moz-transform:rotate(0deg); 200 | -o-transform:rotate(0deg); 201 | transform:rotate(0deg); 202 | -webkit-transition: transform .5s ease-in-out; 203 | -moz-transition: transform .5s ease-in-out; 204 | -o-transition: transform .5s ease-in-out; 205 | transition: transform .5s ease-in-out; 206 | } 207 | 208 | :target a.plus:before { 209 | -webkit-transform:rotate(-135deg); 210 | -moz-transform:rotate(-135deg); 211 | -o-transform:rotate(-135deg); 212 | transform:rotate(-135deg); 213 | -webkit-transition: transform .5s ease-in-out; 214 | -moz-transition: transform .5s ease-in-out; 215 | -o-transition: transform .5s ease-in-out; 216 | transition: transform .5s ease-in-out; 217 | } 218 | 219 | a.plus:after { 220 | content: ""; 221 | display: block; 222 | height: 30px; 223 | width: 1px; 224 | background: #8d9ebf; 225 | position: absolute; 226 | left: 26px; 227 | top:10px; 228 | -webkit-transform:rotate(90deg); 229 | -moz-transform:rotate(90deg); 230 | -o-transform:rotate(90deg); 231 | transform:rotate(90deg); 232 | -webkit-transition: transform .5s ease-in-out; 233 | -moz-transition: transform .5s ease-in-out; 234 | -o-transition: transform .5s ease-in-out; 235 | transition: transform .5s ease-in-out; 236 | } 237 | 238 | :target a.plus:after { 239 | -webkit-transform:rotate(-45deg); 240 | -moz-transform:rotate(-45deg); 241 | -o-transform:rotate(-45deg); 242 | transform:rotate(-45deg); 243 | -webkit-transition: transform .5s ease-in-out; 244 | -moz-transition: transform .5s ease-in-out; 245 | -o-transition: transform .5s ease-in-out; 246 | transition: transform .5s ease-in-out; 247 | } 248 | 249 | circle.githublive-linez-circle { 250 | stroke-width: 2; 251 | -webkit-transition: all 0.15s linear; 252 | -moz-transition: all 0.15s linear; 253 | -o-transition: all 0.15s linear; 254 | transition: all 0.15s linear; 255 | } 256 | 257 | circle.githublive-linez-circle:hover { 258 | stroke-width: 3; 259 | } 260 | 261 | circle.red { 262 | fill: #b00038; 263 | stroke: #ea5b89; 264 | } 265 | 266 | circle.green { 267 | fill: #008f35; 268 | stroke: #4fcb7d; 269 | } 270 | 271 | circle.blue { 272 | fill: #007777; 273 | stroke: #46b3b3; 274 | } 275 | 276 | circle.yellow { 277 | fill: #c69000; 278 | stroke: #ffd564; 279 | } 280 | 281 | circle.default { 282 | fill: #111111; 283 | stroke: #555555; 284 | } 285 | 286 | path.githublive-linez-path { 287 | fill: none !important; 288 | stroke-width: 2px !important; 289 | stroke-linecap: round !important; 290 | } 291 | 292 | path.red { 293 | stroke: #e20048 !important; 294 | } 295 | 296 | path.green { 297 | stroke: #00b945 !important; 298 | } 299 | 300 | path.blue { 301 | stroke: #009999 !important; 302 | } 303 | 304 | path.yellow { 305 | stroke: #ffba00 !important; 306 | } 307 | 308 | path.default { 309 | stroke: #111111 !important; 310 | } 311 | 312 | .d3-tip { 313 | line-height: 150%; 314 | padding: 12px; 315 | font-size: 12px; 316 | max-width: 250px; 317 | background: rgba(0, 0, 0, 0.8); 318 | color: #ffffff; 319 | border-radius: 2px; 320 | box-shadow: 0 0 15px rgba(0, 0, 0, 0.8); 321 | } 322 | 323 | .d3-tip:after { 324 | box-sizing: border-box; 325 | display: inline; 326 | font-size: 10px; 327 | width: 100%; 328 | line-height: 1; 329 | color: rgba(0, 0, 0, 0.8); 330 | content: "\25BC"; 331 | position: absolute; 332 | text-align: center; 333 | } 334 | 335 | .d3-tip.n:after { 336 | margin: -1px 0 0 0; 337 | top: 100%; 338 | left: 0; 339 | } 340 | 341 | .d3-tip em { 342 | color: #999999; 343 | } 344 | 345 | .d3-tip span { 346 | font-weight: bold; 347 | } 348 | 349 | .d3-tip span.red { 350 | color: #e20048; 351 | } 352 | 353 | .d3-tip span.green { 354 | color: #00b945; 355 | } 356 | 357 | .d3-tip span.blue { 358 | color: #009999; 359 | } 360 | 361 | .d3-tip span.yellow { 362 | color: #ffba00; 363 | } 364 | -------------------------------------------------------------------------------- /cartographer/public/gitlive.js: -------------------------------------------------------------------------------- 1 | var map = new Datamap({ 2 | element: document.getElementById('map'), 3 | geographyConfig: { 4 | highlightOnHover: false, 5 | popupOnHover: false, 6 | borderColor: '#555555', 7 | borderWidth: 1 8 | }, 9 | fills: { 10 | defaultFill: '#2e2e2e' 11 | }, 12 | }); 13 | 14 | // have map intercept clicks / taps 15 | document.getElementById('map').addEventListener('click', function() {}); 16 | 17 | var geocoder = new google.maps.Geocoder(); 18 | 19 | var geocode = function(location, success, retries) { 20 | if (typeof(retries) === 'undefined') retries = 3; 21 | geocoder.geocode({'address': location}, function(results, status) { 22 | if (status == google.maps.GeocoderStatus.OK) { 23 | success(results); 24 | } else if (status == google.maps.GeocoderStatus.OVER_QUERY_LIMIT) { 25 | if (retries > 0) { 26 | setTimeout(function() { 27 | geocode(location, success, retries - 1); 28 | }, 2000); 29 | } else { 30 | window.location.reload(); 31 | } 32 | } else { 33 | // ignore 34 | } 35 | }); 36 | } 37 | 38 | var id = 0; 39 | 40 | var locs = []; 41 | 42 | var removeLoc = function(data) { 43 | var index; 44 | for (var i = 0, len = locs.length; i < len; i++) { 45 | if (locs[i].id === data.id) { 46 | index = i; 47 | break; 48 | } 49 | } 50 | locs.splice(index, 1); 51 | } 52 | 53 | var TIMEOUT = 60000; 54 | 55 | var processEvent = function(data) { 56 | var flip = function(data) { 57 | var tmp = data.origin; 58 | data.origin = data.destination; 59 | data.destination = tmp; 60 | } 61 | switch(data.type) { 62 | case 'WatchEvent': 63 | data.options = { color: 'yellow' }; 64 | break; 65 | case 'ForkEvent': 66 | data.options = { color: 'blue' }; 67 | break; 68 | case 'PullRequestEvent': 69 | data.options = { color: 'green' }; 70 | break; 71 | case 'IssuesEvent': 72 | data.options = { color: 'red' }; 73 | break; 74 | } 75 | data.id = id++; 76 | data.timeoutTime = TIMEOUT; 77 | data.timeoutFunc = function() { 78 | removeLoc(data); 79 | map.linez(locs); 80 | } 81 | data.timeout = setTimeout(data.timeoutFunc, data.timeoutTime); 82 | locs.push(data); 83 | map.linez(locs); 84 | }; 85 | 86 | var processRawEvent = function(event) { 87 | geocode(event.fromLocation, function(from) { 88 | geocode(event.toLocation, function(to) { 89 | var fromLoc = { 90 | latitude: from[0].geometry.location.lat(), 91 | longitude: from[0].geometry.location.lng() 92 | }; 93 | var toLoc = { 94 | latitude: to[0].geometry.location.lat(), 95 | longitude: to[0].geometry.location.lng() 96 | } 97 | event.origin = fromLoc; 98 | event.destination = toLoc; 99 | processEvent(event); 100 | }); 101 | }); 102 | }; 103 | 104 | if (typeof(EventSource) !== 'undefined') { 105 | var source = new EventSource('/events'); 106 | source.onmessage = function(e) { 107 | var event = JSON.parse(JSON.parse(e.data)); 108 | processRawEvent(event); 109 | }; 110 | } else { 111 | alert('Your browser is not supported. You must use a browser that supports the EventSource API.'); 112 | } 113 | 114 | var escapeHtml = function(str) { 115 | var div = document.createElement('div'); 116 | div.appendChild(document.createTextNode(str)); 117 | return div.innerHTML; 118 | } 119 | 120 | var linezOptions = { 121 | arcSharpness: 1, 122 | highlightBorderWidth: 3, 123 | popupTemplate: function(data) { 124 | // remember to escape data 125 | var user = escapeHtml(data.fromLogin); 126 | var from = escapeHtml(data.fromLocation); 127 | var repo = escapeHtml(data.toLogin) + '/' + escapeHtml(data.toRepo); 128 | var to = escapeHtml(data.toLocation); 129 | var message; 130 | switch (data.type) { 131 | case 'WatchEvent': 132 | message = 'starred'; 133 | break; 134 | case 'ForkEvent': 135 | message = 'forked'; 136 | break; 137 | case 'PullRequestEvent': 138 | message = 'sent a pull request to'; 139 | break; 140 | case 'IssuesEvent': 141 | message = 'opened an issue at'; 142 | break; 143 | } 144 | return user + ' (' + from + ') ' + message + ' ' + repo + ' (' + to + ')'; 145 | } 146 | }; 147 | 148 | var mobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); 149 | 150 | var handleLinez = function(layer, data) { 151 | var self = this; 152 | var svg = this.svg; 153 | 154 | var tip = d3.tip().attr('class', 'd3-tip').offset([-10, 0]).html(linezOptions.popupTemplate); 155 | layer.call(tip); 156 | 157 | var linez = layer.selectAll('.githublive-linez').data(data, JSON.stringify); 158 | 159 | var container = linez.enter().append('g').attr('class', 'githublive-linez'); 160 | 161 | var back = container.append('g'); 162 | var front = container.append('g'); 163 | 164 | front.append('svg:circle') 165 | .attr('class', 'githublive-linez-circle') 166 | .attr('cx', function(datum) { 167 | var latLng = self.latLngToXY(datum.origin.latitude, datum.origin.longitude); 168 | if (latLng) { 169 | return latLng[0]; 170 | } 171 | }) 172 | .attr('cy', function(datum) { 173 | var latLng = self.latLngToXY(datum.origin.latitude, datum.origin.longitude); 174 | if (latLng) { 175 | return latLng[1]; 176 | } 177 | }) 178 | .attr('r', 0) 179 | .attr('data-info', function(d) { 180 | return JSON.stringify(d); 181 | }) 182 | .attr('class', function(datum) { 183 | var previous = d3.select(this).attr('class'); 184 | if (datum.options && datum.options.color) { 185 | var ret = previous + ' ' + datum.options.color; 186 | return ret; 187 | } 188 | return previous + ' default'; 189 | }) 190 | .on('mouseover', function(datum) { 191 | clearTimeout(datum.timeout); 192 | 193 | tip.show(datum); 194 | }) 195 | .on('mouseout', function(datum) { 196 | datum.timeout = setTimeout(datum.timeoutFunc, datum.timeoutTime); 197 | 198 | tip.hide(datum); 199 | }) 200 | .on('click', function(datum) { 201 | if (!mobileDevice) { 202 | window.open(datum.url, '_blank'); 203 | } 204 | }) 205 | .transition().delay(0).duration(400) 206 | .attr('r', 10) 207 | .transition().delay(400).duration(200) 208 | .attr('r', 5); 209 | 210 | back.append('svg:path') 211 | .attr('class', 'githublive-linez-path') 212 | .attr('class', function(datum) { 213 | var previous = d3.select(this).attr('class'); 214 | if (datum.options && datum.options.color) { 215 | var ret = previous + ' ' + datum.options.color; 216 | return ret; 217 | } 218 | return previous + ' default'; 219 | }) 220 | .attr('d', function(datum) { 221 | var originXY = self.latLngToXY(datum.origin.latitude, datum.origin.longitude); 222 | var destXY = self.latLngToXY(datum.destination.latitude, datum.destination.longitude); 223 | var midXY = [ (originXY[0] + destXY[0]) / 2, (originXY[1] + destXY[1]) / 2]; 224 | return "M" + originXY[0] + ',' + originXY[1] + "S" + (midXY[0] + (50 * linezOptions.arcSharpness)) + "," + (midXY[1] - (75 * linezOptions.arcSharpness)) + "," + destXY[0] + "," + destXY[1]; 225 | }) 226 | .attr('stroke-dasharray', function() { 227 | var length = this.getTotalLength(); 228 | return length + ' ' + length; 229 | }) 230 | .attr('stroke-dashoffset', function() { 231 | var length = this.getTotalLength(); 232 | return length; 233 | }) 234 | .transition() 235 | .delay(400) 236 | .duration(600) 237 | .ease('linear') 238 | .attr('stroke-dashoffset', 0); 239 | 240 | front.append('svg:circle') 241 | .attr('class', 'githublive-linez-circle') 242 | .attr('cx', function(datum) { 243 | var latLng = self.latLngToXY(datum.destination.latitude, datum.destination.longitude); 244 | if (latLng) { 245 | return latLng[0]; 246 | } 247 | }) 248 | .attr('cy', function(datum) { 249 | var latLng = self.latLngToXY(datum.destination.latitude, datum.destination.longitude); 250 | if (latLng) { 251 | return latLng[1]; 252 | } 253 | }) 254 | .attr('r', 0) 255 | .attr('data-info', function(d) { 256 | return JSON.stringify(d); 257 | }) 258 | .attr('class', function(datum) { 259 | var previous = d3.select(this).attr('class'); 260 | if (datum.options && datum.options.color) { 261 | var ret = previous + ' ' + datum.options.color; 262 | return ret; 263 | } 264 | return previous + ' default'; 265 | }) 266 | .on('mouseover', function(datum) { 267 | clearTimeout(datum.timeout); 268 | 269 | tip.show(datum); 270 | }) 271 | .on('mouseout', function(datum) { 272 | datum.timeout = setTimeout(datum.timeoutFunc, datum.timeoutTime); 273 | 274 | tip.hide(datum); 275 | }) 276 | .on('click', function(datum) { 277 | if (!mobileDevice) { 278 | window.open(datum.url, '_blank'); 279 | } 280 | }) 281 | .transition().delay(1000).duration(400) 282 | .attr('r', 10) 283 | .transition().delay(1400).duration(200) 284 | .attr('r', 5); 285 | 286 | linez.exit() 287 | .transition() 288 | .style('opacity', 0) 289 | .remove(); 290 | }; 291 | 292 | map.addPlugin('linez', handleLinez); 293 | -------------------------------------------------------------------------------- /slurp/src/main/java/org/json/CDL.java: -------------------------------------------------------------------------------- 1 | package org.json; 2 | 3 | /* 4 | Copyright (c) 2002 JSON.org 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | The Software shall be used for Good, not Evil. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | */ 26 | 27 | /** 28 | * This provides static methods to convert comma delimited text into a 29 | * JSONArray, and to covert a JSONArray into comma delimited text. Comma 30 | * delimited text is a very popular format for data interchange. It is 31 | * understood by most database, spreadsheet, and organizer programs. 32 | *

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

38 | * The first row usually contains the names of the columns. 39 | *

40 | * A comma delimited list can be converted into a JSONArray of JSONObjects. 41 | * The names for the elements in the JSONObjects can be taken from the names 42 | * in the first row. 43 | * @author JSON.org 44 | * @version 2012-11-13 45 | */ 46 | public class CDL { 47 | 48 | /** 49 | * Get the next value. The value can be wrapped in quotes. The value can 50 | * be empty. 51 | * @param x A JSONTokener of the source text. 52 | * @return The value string, or null if empty. 53 | * @throws JSONException if the quoted string is badly formed. 54 | */ 55 | private static String getValue(JSONTokener x) throws JSONException { 56 | char c; 57 | char q; 58 | StringBuffer sb; 59 | do { 60 | c = x.next(); 61 | } while (c == ' ' || c == '\t'); 62 | switch (c) { 63 | case 0: 64 | return null; 65 | case '"': 66 | case '\'': 67 | q = c; 68 | sb = new StringBuffer(); 69 | for (;;) { 70 | c = x.next(); 71 | if (c == q) { 72 | break; 73 | } 74 | if (c == 0 || c == '\n' || c == '\r') { 75 | throw x.syntaxError("Missing close quote '" + q + "'."); 76 | } 77 | sb.append(c); 78 | } 79 | return sb.toString(); 80 | case ',': 81 | x.back(); 82 | return ""; 83 | default: 84 | x.back(); 85 | return x.nextTo(','); 86 | } 87 | } 88 | 89 | /** 90 | * Produce a JSONArray of strings from a row of comma delimited values. 91 | * @param x A JSONTokener of the source text. 92 | * @return A JSONArray of strings. 93 | * @throws JSONException 94 | */ 95 | public static JSONArray rowToJSONArray(JSONTokener x) throws JSONException { 96 | JSONArray ja = new JSONArray(); 97 | for (;;) { 98 | String value = getValue(x); 99 | char c = x.next(); 100 | if (value == null || 101 | (ja.length() == 0 && value.length() == 0 && c != ',')) { 102 | return null; 103 | } 104 | ja.put(value); 105 | for (;;) { 106 | if (c == ',') { 107 | break; 108 | } 109 | if (c != ' ') { 110 | if (c == '\n' || c == '\r' || c == 0) { 111 | return ja; 112 | } 113 | throw x.syntaxError("Bad character '" + c + "' (" + 114 | (int)c + ")."); 115 | } 116 | c = x.next(); 117 | } 118 | } 119 | } 120 | 121 | /** 122 | * Produce a JSONObject from a row of comma delimited text, using a 123 | * parallel JSONArray of strings to provides the names of the elements. 124 | * @param names A JSONArray of names. This is commonly obtained from the 125 | * first row of a comma delimited text file using the rowToJSONArray 126 | * method. 127 | * @param x A JSONTokener of the source text. 128 | * @return A JSONObject combining the names and values. 129 | * @throws JSONException 130 | */ 131 | public static JSONObject rowToJSONObject(JSONArray names, JSONTokener x) 132 | throws JSONException { 133 | JSONArray ja = rowToJSONArray(x); 134 | return ja != null ? ja.toJSONObject(names) : null; 135 | } 136 | 137 | /** 138 | * Produce a comma delimited text row from a JSONArray. Values containing 139 | * the comma character will be quoted. Troublesome characters may be 140 | * removed. 141 | * @param ja A JSONArray of strings. 142 | * @return A string ending in NEWLINE. 143 | */ 144 | public static String rowToString(JSONArray ja) { 145 | StringBuffer sb = new StringBuffer(); 146 | for (int i = 0; i < ja.length(); i += 1) { 147 | if (i > 0) { 148 | sb.append(','); 149 | } 150 | Object object = ja.opt(i); 151 | if (object != null) { 152 | String string = object.toString(); 153 | if (string.length() > 0 && (string.indexOf(',') >= 0 || 154 | string.indexOf('\n') >= 0 || string.indexOf('\r') >= 0 || 155 | string.indexOf(0) >= 0 || string.charAt(0) == '"')) { 156 | sb.append('"'); 157 | int length = string.length(); 158 | for (int j = 0; j < length; j += 1) { 159 | char c = string.charAt(j); 160 | if (c >= ' ' && c != '"') { 161 | sb.append(c); 162 | } 163 | } 164 | sb.append('"'); 165 | } else { 166 | sb.append(string); 167 | } 168 | } 169 | } 170 | sb.append('\n'); 171 | return sb.toString(); 172 | } 173 | 174 | /** 175 | * Produce a JSONArray of JSONObjects from a comma delimited text string, 176 | * using the first row as a source of names. 177 | * @param string The comma delimited text. 178 | * @return A JSONArray of JSONObjects. 179 | * @throws JSONException 180 | */ 181 | public static JSONArray toJSONArray(String string) throws JSONException { 182 | return toJSONArray(new JSONTokener(string)); 183 | } 184 | 185 | /** 186 | * Produce a JSONArray of JSONObjects from a comma delimited text string, 187 | * using the first row as a source of names. 188 | * @param x The JSONTokener containing the comma delimited text. 189 | * @return A JSONArray of JSONObjects. 190 | * @throws JSONException 191 | */ 192 | public static JSONArray toJSONArray(JSONTokener x) throws JSONException { 193 | return toJSONArray(rowToJSONArray(x), x); 194 | } 195 | 196 | /** 197 | * Produce a JSONArray of JSONObjects from a comma delimited text string 198 | * using a supplied JSONArray as the source of element names. 199 | * @param names A JSONArray of strings. 200 | * @param string The comma delimited text. 201 | * @return A JSONArray of JSONObjects. 202 | * @throws JSONException 203 | */ 204 | public static JSONArray toJSONArray(JSONArray names, String string) 205 | throws JSONException { 206 | return toJSONArray(names, new JSONTokener(string)); 207 | } 208 | 209 | /** 210 | * Produce a JSONArray of JSONObjects from a comma delimited text string 211 | * using a supplied JSONArray as the source of element names. 212 | * @param names A JSONArray of strings. 213 | * @param x A JSONTokener of the source text. 214 | * @return A JSONArray of JSONObjects. 215 | * @throws JSONException 216 | */ 217 | public static JSONArray toJSONArray(JSONArray names, JSONTokener x) 218 | throws JSONException { 219 | if (names == null || names.length() == 0) { 220 | return null; 221 | } 222 | JSONArray ja = new JSONArray(); 223 | for (;;) { 224 | JSONObject jo = rowToJSONObject(names, x); 225 | if (jo == null) { 226 | break; 227 | } 228 | ja.put(jo); 229 | } 230 | if (ja.length() == 0) { 231 | return null; 232 | } 233 | return ja; 234 | } 235 | 236 | 237 | /** 238 | * Produce a comma delimited text from a JSONArray of JSONObjects. The 239 | * first row will be a list of names obtained by inspecting the first 240 | * JSONObject. 241 | * @param ja A JSONArray of JSONObjects. 242 | * @return A comma delimited text. 243 | * @throws JSONException 244 | */ 245 | public static String toString(JSONArray ja) throws JSONException { 246 | JSONObject jo = ja.optJSONObject(0); 247 | if (jo != null) { 248 | JSONArray names = jo.names(); 249 | if (names != null) { 250 | return rowToString(names) + toString(names, ja); 251 | } 252 | } 253 | return null; 254 | } 255 | 256 | /** 257 | * Produce a comma delimited text from a JSONArray of JSONObjects using 258 | * a provided list of names. The list of names is not included in the 259 | * output. 260 | * @param names A JSONArray of strings. 261 | * @param ja A JSONArray of JSONObjects. 262 | * @return A comma delimited text. 263 | * @throws JSONException 264 | */ 265 | public static String toString(JSONArray names, JSONArray ja) 266 | throws JSONException { 267 | if (names == null || names.length() == 0) { 268 | return null; 269 | } 270 | StringBuffer sb = new StringBuffer(); 271 | for (int i = 0; i < ja.length(); i += 1) { 272 | JSONObject jo = ja.optJSONObject(i); 273 | if (jo != null) { 274 | sb.append(rowToString(jo.toJSONArray(names))); 275 | } 276 | } 277 | return sb.toString(); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /slurp/src/main/java/org/json/JSONWriter.java: -------------------------------------------------------------------------------- 1 | package org.json; 2 | 3 | import java.io.IOException; 4 | import java.io.Writer; 5 | 6 | /* 7 | Copyright (c) 2006 JSON.org 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | The Software shall be used for Good, not Evil. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | SOFTWARE. 28 | */ 29 | 30 | /** 31 | * JSONWriter provides a quick and convenient way of producing JSON text. 32 | * The texts produced strictly conform to JSON syntax rules. No whitespace is 33 | * added, so the results are ready for transmission or storage. Each instance of 34 | * JSONWriter can produce one JSON text. 35 | *

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

 44 |  * new JSONWriter(myWriter)
 45 |  *     .object()
 46 |  *         .key("JSON")
 47 |  *         .value("Hello, World!")
 48 |  *     .endObject();
which writes
 49 |  * {"JSON":"Hello, World!"}
50 | *

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

55 | * This can sometimes be easier than using a JSONObject to build a string. 56 | * @author JSON.org 57 | * @version 2011-11-24 58 | */ 59 | public class JSONWriter { 60 | private static final int maxdepth = 200; 61 | 62 | /** 63 | * The comma flag determines if a comma should be output before the next 64 | * value. 65 | */ 66 | private boolean comma; 67 | 68 | /** 69 | * The current mode. Values: 70 | * 'a' (array), 71 | * 'd' (done), 72 | * 'i' (initial), 73 | * 'k' (key), 74 | * 'o' (object). 75 | */ 76 | protected char mode; 77 | 78 | /** 79 | * The object/array stack. 80 | */ 81 | private final JSONObject stack[]; 82 | 83 | /** 84 | * The stack top index. A value of 0 indicates that the stack is empty. 85 | */ 86 | private int top; 87 | 88 | /** 89 | * The writer that will receive the output. 90 | */ 91 | protected Writer writer; 92 | 93 | /** 94 | * Make a fresh JSONWriter. It can be used to build one JSON text. 95 | */ 96 | public JSONWriter(Writer w) { 97 | this.comma = false; 98 | this.mode = 'i'; 99 | this.stack = new JSONObject[maxdepth]; 100 | this.top = 0; 101 | this.writer = w; 102 | } 103 | 104 | /** 105 | * Append a value. 106 | * @param string A string value. 107 | * @return this 108 | * @throws JSONException If the value is out of sequence. 109 | */ 110 | private JSONWriter append(String string) throws JSONException { 111 | if (string == null) { 112 | throw new JSONException("Null pointer"); 113 | } 114 | if (this.mode == 'o' || this.mode == 'a') { 115 | try { 116 | if (this.comma && this.mode == 'a') { 117 | this.writer.write(','); 118 | } 119 | this.writer.write(string); 120 | } catch (IOException e) { 121 | throw new JSONException(e); 122 | } 123 | if (this.mode == 'o') { 124 | this.mode = 'k'; 125 | } 126 | this.comma = true; 127 | return this; 128 | } 129 | throw new JSONException("Value out of sequence."); 130 | } 131 | 132 | /** 133 | * Begin appending a new array. All values until the balancing 134 | * endArray will be appended to this array. The 135 | * endArray method must be called to mark the array's end. 136 | * @return this 137 | * @throws JSONException If the nesting is too deep, or if the object is 138 | * started in the wrong place (for example as a key or after the end of the 139 | * outermost array or object). 140 | */ 141 | public JSONWriter array() throws JSONException { 142 | if (this.mode == 'i' || this.mode == 'o' || this.mode == 'a') { 143 | this.push(null); 144 | this.append("["); 145 | this.comma = false; 146 | return this; 147 | } 148 | throw new JSONException("Misplaced array."); 149 | } 150 | 151 | /** 152 | * End something. 153 | * @param mode Mode 154 | * @param c Closing character 155 | * @return this 156 | * @throws JSONException If unbalanced. 157 | */ 158 | private JSONWriter end(char mode, char c) throws JSONException { 159 | if (this.mode != mode) { 160 | throw new JSONException(mode == 'a' 161 | ? "Misplaced endArray." 162 | : "Misplaced endObject."); 163 | } 164 | this.pop(mode); 165 | try { 166 | this.writer.write(c); 167 | } catch (IOException e) { 168 | throw new JSONException(e); 169 | } 170 | this.comma = true; 171 | return this; 172 | } 173 | 174 | /** 175 | * End an array. This method most be called to balance calls to 176 | * array. 177 | * @return this 178 | * @throws JSONException If incorrectly nested. 179 | */ 180 | public JSONWriter endArray() throws JSONException { 181 | return this.end('a', ']'); 182 | } 183 | 184 | /** 185 | * End an object. This method most be called to balance calls to 186 | * object. 187 | * @return this 188 | * @throws JSONException If incorrectly nested. 189 | */ 190 | public JSONWriter endObject() throws JSONException { 191 | return this.end('k', '}'); 192 | } 193 | 194 | /** 195 | * Append a key. The key will be associated with the next value. In an 196 | * object, every value must be preceded by a key. 197 | * @param string A key string. 198 | * @return this 199 | * @throws JSONException If the key is out of place. For example, keys 200 | * do not belong in arrays or if the key is null. 201 | */ 202 | public JSONWriter key(String string) throws JSONException { 203 | if (string == null) { 204 | throw new JSONException("Null key."); 205 | } 206 | if (this.mode == 'k') { 207 | try { 208 | this.stack[this.top - 1].putOnce(string, Boolean.TRUE); 209 | if (this.comma) { 210 | this.writer.write(','); 211 | } 212 | this.writer.write(JSONObject.quote(string)); 213 | this.writer.write(':'); 214 | this.comma = false; 215 | this.mode = 'o'; 216 | return this; 217 | } catch (IOException e) { 218 | throw new JSONException(e); 219 | } 220 | } 221 | throw new JSONException("Misplaced key."); 222 | } 223 | 224 | 225 | /** 226 | * Begin appending a new object. All keys and values until the balancing 227 | * endObject will be appended to this object. The 228 | * endObject method must be called to mark the object's end. 229 | * @return this 230 | * @throws JSONException If the nesting is too deep, or if the object is 231 | * started in the wrong place (for example as a key or after the end of the 232 | * outermost array or object). 233 | */ 234 | public JSONWriter object() throws JSONException { 235 | if (this.mode == 'i') { 236 | this.mode = 'o'; 237 | } 238 | if (this.mode == 'o' || this.mode == 'a') { 239 | this.append("{"); 240 | this.push(new JSONObject()); 241 | this.comma = false; 242 | return this; 243 | } 244 | throw new JSONException("Misplaced object."); 245 | 246 | } 247 | 248 | 249 | /** 250 | * Pop an array or object scope. 251 | * @param c The scope to close. 252 | * @throws JSONException If nesting is wrong. 253 | */ 254 | private void pop(char c) throws JSONException { 255 | if (this.top <= 0) { 256 | throw new JSONException("Nesting error."); 257 | } 258 | char m = this.stack[this.top - 1] == null ? 'a' : 'k'; 259 | if (m != c) { 260 | throw new JSONException("Nesting error."); 261 | } 262 | this.top -= 1; 263 | this.mode = this.top == 0 264 | ? 'd' 265 | : this.stack[this.top - 1] == null 266 | ? 'a' 267 | : 'k'; 268 | } 269 | 270 | /** 271 | * Push an array or object scope. 272 | * @param c The scope to open. 273 | * @throws JSONException If nesting is too deep. 274 | */ 275 | private void push(JSONObject jo) throws JSONException { 276 | if (this.top >= maxdepth) { 277 | throw new JSONException("Nesting too deep."); 278 | } 279 | this.stack[this.top] = jo; 280 | this.mode = jo == null ? 'a' : 'k'; 281 | this.top += 1; 282 | } 283 | 284 | 285 | /** 286 | * Append either the value true or the value 287 | * false. 288 | * @param b A boolean. 289 | * @return this 290 | * @throws JSONException 291 | */ 292 | public JSONWriter value(boolean b) throws JSONException { 293 | return this.append(b ? "true" : "false"); 294 | } 295 | 296 | /** 297 | * Append a double value. 298 | * @param d A double. 299 | * @return this 300 | * @throws JSONException If the number is not finite. 301 | */ 302 | public JSONWriter value(double d) throws JSONException { 303 | return this.value(new Double(d)); 304 | } 305 | 306 | /** 307 | * Append a long value. 308 | * @param l A long. 309 | * @return this 310 | * @throws JSONException 311 | */ 312 | public JSONWriter value(long l) throws JSONException { 313 | return this.append(Long.toString(l)); 314 | } 315 | 316 | 317 | /** 318 | * Append an object value. 319 | * @param object The object to append. It can be null, or a Boolean, Number, 320 | * String, JSONObject, or JSONArray, or an object that implements JSONString. 321 | * @return this 322 | * @throws JSONException If the value is out of sequence. 323 | */ 324 | public JSONWriter value(Object object) throws JSONException { 325 | return this.append(JSONObject.valueToString(object)); 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /slurp/src/main/java/org/json/XMLTokener.java: -------------------------------------------------------------------------------- 1 | package org.json; 2 | 3 | /* 4 | Copyright (c) 2002 JSON.org 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | The Software shall be used for Good, not Evil. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | */ 26 | 27 | /** 28 | * The XMLTokener extends the JSONTokener to provide additional methods 29 | * for the parsing of XML texts. 30 | * @author JSON.org 31 | * @version 2012-11-13 32 | */ 33 | public class XMLTokener extends JSONTokener { 34 | 35 | 36 | /** The table of entity values. It initially contains Character values for 37 | * amp, apos, gt, lt, quot. 38 | */ 39 | public static final java.util.HashMap entity; 40 | 41 | static { 42 | entity = new java.util.HashMap<>(8); 43 | entity.put("amp", XML.AMP); 44 | entity.put("apos", XML.APOS); 45 | entity.put("gt", XML.GT); 46 | entity.put("lt", XML.LT); 47 | entity.put("quot", XML.QUOT); 48 | } 49 | 50 | /** 51 | * Construct an XMLTokener from a string. 52 | * @param s A source string. 53 | */ 54 | public XMLTokener(String s) { 55 | super(s); 56 | } 57 | 58 | /** 59 | * Get the text in the CDATA block. 60 | * @return The string up to the ]]>. 61 | * @throws JSONException If the ]]> is not found. 62 | */ 63 | public String nextCDATA() throws JSONException { 64 | char c; 65 | int i; 66 | StringBuffer sb = new StringBuffer(); 67 | for (;;) { 68 | c = next(); 69 | if (end()) { 70 | throw syntaxError("Unclosed CDATA"); 71 | } 72 | sb.append(c); 73 | i = sb.length() - 3; 74 | if (i >= 0 && sb.charAt(i) == ']' && 75 | sb.charAt(i + 1) == ']' && sb.charAt(i + 2) == '>') { 76 | sb.setLength(i); 77 | return sb.toString(); 78 | } 79 | } 80 | } 81 | 82 | 83 | /** 84 | * Get the next XML outer token, trimming whitespace. There are two kinds 85 | * of tokens: the '<' character which begins a markup tag, and the content 86 | * text between markup tags. 87 | * 88 | * @return A string, or a '<' Character, or null if there is no more 89 | * source text. 90 | * @throws JSONException 91 | */ 92 | public Object nextContent() throws JSONException { 93 | char c; 94 | StringBuffer sb; 95 | do { 96 | c = next(); 97 | } while (Character.isWhitespace(c)); 98 | if (c == 0) { 99 | return null; 100 | } 101 | if (c == '<') { 102 | return XML.LT; 103 | } 104 | sb = new StringBuffer(); 105 | for (;;) { 106 | if (c == '<' || c == 0) { 107 | back(); 108 | return sb.toString().trim(); 109 | } 110 | if (c == '&') { 111 | sb.append(nextEntity(c)); 112 | } else { 113 | sb.append(c); 114 | } 115 | c = next(); 116 | } 117 | } 118 | 119 | 120 | /** 121 | * Return the next entity. These entities are translated to Characters: 122 | * & ' > < ". 123 | * @param ampersand An ampersand character. 124 | * @return A Character or an entity String if the entity is not recognized. 125 | * @throws JSONException If missing ';' in XML entity. 126 | */ 127 | public Object nextEntity(char ampersand) throws JSONException { 128 | StringBuffer sb = new StringBuffer(); 129 | for (;;) { 130 | char c = next(); 131 | if (Character.isLetterOrDigit(c) || c == '#') { 132 | sb.append(Character.toLowerCase(c)); 133 | } else if (c == ';') { 134 | break; 135 | } else { 136 | throw syntaxError("Missing ';' in XML entity: &" + sb); 137 | } 138 | } 139 | String string = sb.toString(); 140 | Object object = entity.get(string); 141 | return object != null ? object : ampersand + string + ";"; 142 | } 143 | 144 | 145 | /** 146 | * Returns the next XML meta token. This is used for skipping over 147 | * and structures. 148 | * @return Syntax characters (< > / = ! ?) are returned as 149 | * Character, and strings and names are returned as Boolean. We don't care 150 | * what the values actually are. 151 | * @throws JSONException If a string is not properly closed or if the XML 152 | * is badly structured. 153 | */ 154 | public Object nextMeta() throws JSONException { 155 | char c; 156 | char q; 157 | do { 158 | c = next(); 159 | } while (Character.isWhitespace(c)); 160 | switch (c) { 161 | case 0: 162 | throw syntaxError("Misshaped meta tag"); 163 | case '<': 164 | return XML.LT; 165 | case '>': 166 | return XML.GT; 167 | case '/': 168 | return XML.SLASH; 169 | case '=': 170 | return XML.EQ; 171 | case '!': 172 | return XML.BANG; 173 | case '?': 174 | return XML.QUEST; 175 | case '"': 176 | case '\'': 177 | q = c; 178 | for (;;) { 179 | c = next(); 180 | if (c == 0) { 181 | throw syntaxError("Unterminated string"); 182 | } 183 | if (c == q) { 184 | return Boolean.TRUE; 185 | } 186 | } 187 | default: 188 | for (;;) { 189 | c = next(); 190 | if (Character.isWhitespace(c)) { 191 | return Boolean.TRUE; 192 | } 193 | switch (c) { 194 | case 0: 195 | case '<': 196 | case '>': 197 | case '/': 198 | case '=': 199 | case '!': 200 | case '?': 201 | case '"': 202 | case '\'': 203 | back(); 204 | return Boolean.TRUE; 205 | } 206 | } 207 | } 208 | } 209 | 210 | 211 | /** 212 | * Get the next XML Token. These tokens are found inside of angle 213 | * brackets. It may be one of these characters: / > = ! ? or it 214 | * may be a string wrapped in single quotes or double quotes, or it may be a 215 | * name. 216 | * @return a String or a Character. 217 | * @throws JSONException If the XML is not well formed. 218 | */ 219 | public Object nextToken() throws JSONException { 220 | char c; 221 | char q; 222 | StringBuffer sb; 223 | do { 224 | c = next(); 225 | } while (Character.isWhitespace(c)); 226 | switch (c) { 227 | case 0: 228 | throw syntaxError("Misshaped element"); 229 | case '<': 230 | throw syntaxError("Misplaced '<'"); 231 | case '>': 232 | return XML.GT; 233 | case '/': 234 | return XML.SLASH; 235 | case '=': 236 | return XML.EQ; 237 | case '!': 238 | return XML.BANG; 239 | case '?': 240 | return XML.QUEST; 241 | 242 | // Quoted string 243 | 244 | case '"': 245 | case '\'': 246 | q = c; 247 | sb = new StringBuffer(); 248 | for (;;) { 249 | c = next(); 250 | if (c == 0) { 251 | throw syntaxError("Unterminated string"); 252 | } 253 | if (c == q) { 254 | return sb.toString(); 255 | } 256 | if (c == '&') { 257 | sb.append(nextEntity(c)); 258 | } else { 259 | sb.append(c); 260 | } 261 | } 262 | default: 263 | 264 | // Name 265 | 266 | sb = new StringBuffer(); 267 | for (;;) { 268 | sb.append(c); 269 | c = next(); 270 | if (Character.isWhitespace(c)) { 271 | return sb.toString(); 272 | } 273 | switch (c) { 274 | case 0: 275 | return sb.toString(); 276 | case '>': 277 | case '/': 278 | case '=': 279 | case '!': 280 | case '?': 281 | case '[': 282 | case ']': 283 | back(); 284 | return sb.toString(); 285 | case '<': 286 | case '"': 287 | case '\'': 288 | throw syntaxError("Bad character in a name"); 289 | } 290 | } 291 | } 292 | } 293 | 294 | 295 | /** 296 | * Skip characters until past the requested string. 297 | * If it is not found, we are left at the end of the source with a result of false. 298 | * @param to A string to skip past. 299 | * @throws JSONException 300 | */ 301 | public boolean skipPast(String to) throws JSONException { 302 | boolean b; 303 | char c; 304 | int i; 305 | int j; 306 | int offset = 0; 307 | int length = to.length(); 308 | char[] circle = new char[length]; 309 | 310 | /* 311 | * First fill the circle buffer with as many characters as are in the 312 | * to string. If we reach an early end, bail. 313 | */ 314 | 315 | for (i = 0; i < length; i += 1) { 316 | c = next(); 317 | if (c == 0) { 318 | return false; 319 | } 320 | circle[i] = c; 321 | } 322 | 323 | /* We will loop, possibly for all of the remaining characters. */ 324 | 325 | for (;;) { 326 | j = offset; 327 | b = true; 328 | 329 | /* Compare the circle buffer with the to string. */ 330 | 331 | for (i = 0; i < length; i += 1) { 332 | if (circle[j] != to.charAt(i)) { 333 | b = false; 334 | break; 335 | } 336 | j += 1; 337 | if (j >= length) { 338 | j -= length; 339 | } 340 | } 341 | 342 | /* If we exit the loop with b intact, then victory is ours. */ 343 | 344 | if (b) { 345 | return true; 346 | } 347 | 348 | /* Get the next character. If there isn't one, then defeat is ours. */ 349 | 350 | c = next(); 351 | if (c == 0) { 352 | return false; 353 | } 354 | /* 355 | * Shove the character in the circle buffer and advance the 356 | * circle offset. The offset is mod n. 357 | */ 358 | circle[offset] = c; 359 | offset += 1; 360 | if (offset >= length) { 361 | offset -= length; 362 | } 363 | } 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /slurp/src/main/java/org/json/Kim.java: -------------------------------------------------------------------------------- 1 | package org.json; 2 | 3 | 4 | /* 5 | Copyright (c) 2013 JSON.org 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | The Software shall be used for Good, not Evil. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | */ 27 | 28 | /** 29 | * Kim makes immutable eight bit Unicode strings. If the MSB of a byte is set, 30 | * then the next byte is a continuation byte. The last byte of a character 31 | * never has the MSB reset. Every byte that is not the last byte has the MSB 32 | * set. Kim stands for "Keep it minimal". A Unicode character is never longer 33 | * than 3 bytes. Every byte contributes 7 bits to the character. ASCII is 34 | * unmodified. 35 | * 36 | * Kim UTF-8 37 | * one byte U+007F U+007F 38 | * two bytes U+3FFF U+07FF 39 | * three bytes U+10FFF U+FFFF 40 | * four bytes U+10FFFF 41 | * 42 | * Characters in the ranges U+0800..U+3FFF and U+10000..U+10FFFF will be one 43 | * byte smaller when encoded in Kim compared to UTF-8. 44 | * 45 | * Kim is beneficial when using scripts such as Old South Arabian, Aramaic, 46 | * Avestan, Balinese, Batak, Bopomofo, Buginese, Buhid, Carian, Cherokee, 47 | * Coptic, Cyrillic, Deseret, Egyptian Hieroglyphs, Ethiopic, Georgian, 48 | * Glagolitic, Gothic, Hangul Jamo, Hanunoo, Hiragana, Kanbun, Kaithi, 49 | * Kannada, Katakana, Kharoshthi, Khmer, Lao, Lepcha, Limbu, Lycian, Lydian, 50 | * Malayalam, Mandaic, Meroitic, Miao, Mongolian, Myanmar, New Tai Lue, 51 | * Ol Chiki, Old Turkic, Oriya, Osmanya, Pahlavi, Parthian, Phags-Pa, 52 | * Phoenician, Samaritan, Sharada, Sinhala, Sora Sompeng, Tagalog, Tagbanwa, 53 | * Takri, Tai Le, Tai Tham, Tamil, Telugu, Thai, Tibetan, Tifinagh, UCAS. 54 | * 55 | * A kim object can be constructed from an ordinary UTF-16 string, or from a 56 | * byte array. A kim object can produce a UTF-16 string. 57 | * 58 | * As with UTF-8, it is possible to detect character boundaries within a byte 59 | * sequence. UTF-8 is one of the world's great inventions. While Kim is more 60 | * efficient, it is not clear that it is worth the expense of transition. 61 | * 62 | * @version 2013-04-18 63 | */ 64 | public class Kim { 65 | 66 | /** 67 | * The byte array containing the kim's content. 68 | */ 69 | private byte[] bytes = null; 70 | 71 | /** 72 | * The kim's hashcode, conforming to Java's hashcode conventions. 73 | */ 74 | private int hashcode = 0; 75 | 76 | /** 77 | * The number of bytes in the kim. The number of bytes can be as much as 78 | * three times the number of characters. 79 | */ 80 | public int length = 0; 81 | 82 | /** 83 | * The memoization of toString(). 84 | */ 85 | private String string = null; 86 | 87 | /** 88 | * Make a kim from a portion of a byte array. 89 | * 90 | * @param bytes 91 | * A byte array. 92 | * @param from 93 | * The index of the first byte. 94 | * @param thru 95 | * The index of the last byte plus one. 96 | */ 97 | public Kim(byte[] bytes, int from, int thru) { 98 | 99 | // As the bytes are copied into the new kim, a hashcode is computed using a 100 | // modified Fletcher code. 101 | 102 | int sum = 1; 103 | int value; 104 | this.hashcode = 0; 105 | this.length = thru - from; 106 | if (this.length > 0) { 107 | this.bytes = new byte[this.length]; 108 | for (int at = 0; at < this.length; at += 1) { 109 | value = (int) bytes[at + from] & 0xFF; 110 | sum += value; 111 | this.hashcode += sum; 112 | this.bytes[at] = (byte) value; 113 | } 114 | this.hashcode += sum << 16; 115 | } 116 | } 117 | 118 | /** 119 | * Make a kim from a byte array. 120 | * 121 | * @param bytes 122 | * The byte array. 123 | * @param length 124 | * The number of bytes. 125 | */ 126 | public Kim(byte[] bytes, int length) { 127 | this(bytes, 0, length); 128 | } 129 | 130 | /** 131 | * Make a new kim from a substring of an existing kim. The coordinates are 132 | * in byte units, not character units. 133 | * 134 | * @param kim 135 | * The source of bytes. 136 | * @param from 137 | * The point at which to take bytes. 138 | * @param thru 139 | * The point at which to stop taking bytes. 140 | * @return the substring 141 | */ 142 | public Kim(Kim kim, int from, int thru) { 143 | this(kim.bytes, from, thru); 144 | } 145 | 146 | /** 147 | * Make a kim from a string. 148 | * 149 | * @param string 150 | * The string. 151 | * @throws JSONException 152 | * if surrogate pair mismatch. 153 | */ 154 | public Kim(String string) throws JSONException { 155 | int stringLength = string.length(); 156 | this.hashcode = 0; 157 | this.length = 0; 158 | 159 | // First pass: Determine the length of the kim, allowing for the UTF-16 160 | // to UTF-32 conversion, and then the UTF-32 to Kim conversion. 161 | 162 | if (stringLength > 0) { 163 | for (int i = 0; i < stringLength; i += 1) { 164 | int c = string.charAt(i); 165 | if (c <= 0x7F) { 166 | this.length += 1; 167 | } else if (c <= 0x3FFF) { 168 | this.length += 2; 169 | } else { 170 | if (c >= 0xD800 && c <= 0xDFFF) { 171 | i += 1; 172 | int d = string.charAt(i); 173 | if (c > 0xDBFF || d < 0xDC00 || d > 0xDFFF) { 174 | throw new JSONException("Bad UTF16"); 175 | } 176 | } 177 | this.length += 3; 178 | } 179 | } 180 | 181 | // Second pass: Allocate a byte array and fill that array with the conversion 182 | // while computing the hashcode. 183 | 184 | this.bytes = new byte[length]; 185 | int at = 0; 186 | int b; 187 | int sum = 1; 188 | for (int i = 0; i < stringLength; i += 1) { 189 | int character = string.charAt(i); 190 | if (character <= 0x7F) { 191 | bytes[at] = (byte) character; 192 | sum += character; 193 | this.hashcode += sum; 194 | at += 1; 195 | } else if (character <= 0x3FFF) { 196 | b = 0x80 | (character >>> 7); 197 | bytes[at] = (byte) b; 198 | sum += b; 199 | this.hashcode += sum; 200 | at += 1; 201 | b = character & 0x7F; 202 | bytes[at] = (byte) b; 203 | sum += b; 204 | this.hashcode += sum; 205 | at += 1; 206 | } else { 207 | if (character >= 0xD800 && character <= 0xDBFF) { 208 | i += 1; 209 | character = (((character & 0x3FF) << 10) | (string 210 | .charAt(i) & 0x3FF)) + 65536; 211 | } 212 | b = 0x80 | (character >>> 14); 213 | bytes[at] = (byte) b; 214 | sum += b; 215 | this.hashcode += sum; 216 | at += 1; 217 | b = 0x80 | ((character >>> 7) & 0xFF); 218 | bytes[at] = (byte) b; 219 | sum += b; 220 | this.hashcode += sum; 221 | at += 1; 222 | b = character & 0x7F; 223 | bytes[at] = (byte) b; 224 | sum += b; 225 | this.hashcode += sum; 226 | at += 1; 227 | } 228 | } 229 | this.hashcode += sum << 16; 230 | } 231 | } 232 | 233 | /** 234 | * Returns the character at the specified index. The index refers to byte 235 | * values and ranges from 0 to length - 1. The index of the next character 236 | * is at index + Kim.characterSize(kim.characterAt(index)). 237 | * 238 | * @param at 239 | * the index of the char value. The first character is at 0. 240 | * @returns a Unicode character between 0 and 0x10FFFF. 241 | * @throws JSONException 242 | * if at does not point to a valid character. 243 | */ 244 | public int characterAt(int at) throws JSONException { 245 | int c = get(at); 246 | if ((c & 0x80) == 0) { 247 | return c; 248 | } 249 | int character; 250 | int c1 = get(at + 1); 251 | if ((c1 & 0x80) == 0) { 252 | character = ((c & 0x7F) << 7) | c1; 253 | if (character > 0x7F) { 254 | return character; 255 | } 256 | } else { 257 | int c2 = get(at + 2); 258 | character = ((c & 0x7F) << 14) | ((c1 & 0x7F) << 7) | c2; 259 | if ((c2 & 0x80) == 0 && character > 0x3FFF && character <= 0x10FFFF 260 | && (character < 0xD800 || character > 0xDFFF)) { 261 | return character; 262 | } 263 | } 264 | throw new JSONException("Bad character at " + at); 265 | } 266 | 267 | /** 268 | * Returns the number of bytes needed to contain the character in Kim 269 | * format. 270 | * 271 | * @param character 272 | * a Unicode character between 0 and 0x10FFFF. 273 | * @return 1, 2, or 3 274 | * @throws JSONException 275 | * if the character is not representable in a kim. 276 | */ 277 | public static int characterSize(int character) throws JSONException { 278 | if (character < 0 || character > 0x10FFFF) { 279 | throw new JSONException("Bad character " + character); 280 | } 281 | return character <= 0x7F ? 1 : character <= 0x3FFF ? 2 : 3; 282 | } 283 | 284 | /** 285 | * Copy the contents of this kim to a byte array. 286 | * 287 | * @param bytes 288 | * A byte array of sufficient size. 289 | * @param at 290 | * The position within the byte array to take the byes. 291 | * @return The position immediately after the copy. 292 | */ 293 | public int copy(byte[] bytes, int at) { 294 | System.arraycopy(this.bytes, 0, bytes, at, this.length); 295 | return at + this.length; 296 | } 297 | 298 | /** 299 | * Two kim objects containing exactly the same bytes in the same order are 300 | * equal to each other. 301 | * 302 | * @param obj 303 | * the other kim with which to compare. 304 | * @returns true if this and obj are both kim objects containing identical 305 | * byte sequences. 306 | */ 307 | public boolean equals(Object obj) { 308 | if (!(obj instanceof Kim)) { 309 | return false; 310 | } 311 | Kim that = (Kim) obj; 312 | if (this == that) { 313 | return true; 314 | } 315 | if (this.hashcode != that.hashcode) { 316 | return false; 317 | } 318 | return java.util.Arrays.equals(this.bytes, that.bytes); 319 | } 320 | 321 | /** 322 | * Get a byte from a kim. 323 | * @param at 324 | * The position of the byte. The first byte is at 0. 325 | * @return The byte. 326 | * @throws JSONException 327 | * if there is no byte at that position. 328 | */ 329 | public int get(int at) throws JSONException { 330 | if (at < 0 || at > this.length) { 331 | throw new JSONException("Bad character at " + at); 332 | } 333 | return ((int) this.bytes[at]) & 0xFF; 334 | } 335 | 336 | /** 337 | * Returns a hash code value for the kim. 338 | */ 339 | public int hashCode() { 340 | return this.hashcode; 341 | } 342 | 343 | /** 344 | * Produce a UTF-16 String from this kim. The number of codepoints in the 345 | * string will not be greater than the number of bytes in the kim, although 346 | * it could be less. 347 | * 348 | * @return The string. A kim memoizes its string representation. 349 | * @throws JSONException 350 | * if the kim is not valid. 351 | */ 352 | public String toString() throws JSONException { 353 | if (this.string == null) { 354 | int c; 355 | int length = 0; 356 | char chars[] = new char[this.length]; 357 | for (int at = 0; at < this.length; at += characterSize(c)) { 358 | c = this.characterAt(at); 359 | if (c < 0x10000) { 360 | chars[length] = (char) c; 361 | length += 1; 362 | } else { 363 | chars[length] = (char) (0xD800 | ((c - 0x10000) >>> 10)); 364 | length += 1; 365 | chars[length] = (char) (0xDC00 | (c & 0x03FF)); 366 | length += 1; 367 | } 368 | } 369 | this.string = new String(chars, 0, length); 370 | } 371 | return this.string; 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /slurp/src/main/java/org/json/JSONTokener.java: -------------------------------------------------------------------------------- 1 | package org.json; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.InputStreamReader; 7 | import java.io.Reader; 8 | import java.io.StringReader; 9 | 10 | /* 11 | Copyright (c) 2002 JSON.org 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy 14 | of this software and associated documentation files (the "Software"), to deal 15 | in the Software without restriction, including without limitation the rights 16 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | copies of the Software, and to permit persons to whom the Software is 18 | furnished to do so, subject to the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be included in all 21 | copies or substantial portions of the Software. 22 | 23 | The Software shall be used for Good, not Evil. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | */ 33 | 34 | /** 35 | * A JSONTokener takes a source string and extracts characters and tokens from 36 | * it. It is used by the JSONObject and JSONArray constructors to parse 37 | * JSON source strings. 38 | * @author JSON.org 39 | * @version 2012-02-16 40 | */ 41 | public class JSONTokener { 42 | 43 | private long character; 44 | private boolean eof; 45 | private long index; 46 | private long line; 47 | private char previous; 48 | private Reader reader; 49 | private boolean usePrevious; 50 | 51 | 52 | /** 53 | * Construct a JSONTokener from a Reader. 54 | * 55 | * @param reader A reader. 56 | */ 57 | public JSONTokener(Reader reader) { 58 | this.reader = reader.markSupported() 59 | ? reader 60 | : new BufferedReader(reader); 61 | this.eof = false; 62 | this.usePrevious = false; 63 | this.previous = 0; 64 | this.index = 0; 65 | this.character = 1; 66 | this.line = 1; 67 | } 68 | 69 | 70 | /** 71 | * Construct a JSONTokener from an InputStream. 72 | */ 73 | public JSONTokener(InputStream inputStream) throws JSONException { 74 | this(new InputStreamReader(inputStream)); 75 | } 76 | 77 | 78 | /** 79 | * Construct a JSONTokener from a string. 80 | * 81 | * @param s A source string. 82 | */ 83 | public JSONTokener(String s) { 84 | this(new StringReader(s)); 85 | } 86 | 87 | 88 | /** 89 | * Back up one character. This provides a sort of lookahead capability, 90 | * so that you can test for a digit or letter before attempting to parse 91 | * the next number or identifier. 92 | */ 93 | public void back() throws JSONException { 94 | if (this.usePrevious || this.index <= 0) { 95 | throw new JSONException("Stepping back two steps is not supported"); 96 | } 97 | this.index -= 1; 98 | this.character -= 1; 99 | this.usePrevious = true; 100 | this.eof = false; 101 | } 102 | 103 | 104 | /** 105 | * Get the hex value of a character (base16). 106 | * @param c A character between '0' and '9' or between 'A' and 'F' or 107 | * between 'a' and 'f'. 108 | * @return An int between 0 and 15, or -1 if c was not a hex digit. 109 | */ 110 | public static int dehexchar(char c) { 111 | if (c >= '0' && c <= '9') { 112 | return c - '0'; 113 | } 114 | if (c >= 'A' && c <= 'F') { 115 | return c - ('A' - 10); 116 | } 117 | if (c >= 'a' && c <= 'f') { 118 | return c - ('a' - 10); 119 | } 120 | return -1; 121 | } 122 | 123 | public boolean end() { 124 | return this.eof && !this.usePrevious; 125 | } 126 | 127 | 128 | /** 129 | * Determine if the source string still contains characters that next() 130 | * can consume. 131 | * @return true if not yet at the end of the source. 132 | */ 133 | public boolean more() throws JSONException { 134 | this.next(); 135 | if (this.end()) { 136 | return false; 137 | } 138 | this.back(); 139 | return true; 140 | } 141 | 142 | 143 | /** 144 | * Get the next character in the source string. 145 | * 146 | * @return The next character, or 0 if past the end of the source string. 147 | */ 148 | public char next() throws JSONException { 149 | int c; 150 | if (this.usePrevious) { 151 | this.usePrevious = false; 152 | c = this.previous; 153 | } else { 154 | try { 155 | c = this.reader.read(); 156 | } catch (IOException exception) { 157 | throw new JSONException(exception); 158 | } 159 | 160 | if (c <= 0) { // End of stream 161 | this.eof = true; 162 | c = 0; 163 | } 164 | } 165 | this.index += 1; 166 | if (this.previous == '\r') { 167 | this.line += 1; 168 | this.character = c == '\n' ? 0 : 1; 169 | } else if (c == '\n') { 170 | this.line += 1; 171 | this.character = 0; 172 | } else { 173 | this.character += 1; 174 | } 175 | this.previous = (char) c; 176 | return this.previous; 177 | } 178 | 179 | 180 | /** 181 | * Consume the next character, and check that it matches a specified 182 | * character. 183 | * @param c The character to match. 184 | * @return The character. 185 | * @throws JSONException if the character does not match. 186 | */ 187 | public char next(char c) throws JSONException { 188 | char n = this.next(); 189 | if (n != c) { 190 | throw this.syntaxError("Expected '" + c + "' and instead saw '" + 191 | n + "'"); 192 | } 193 | return n; 194 | } 195 | 196 | 197 | /** 198 | * Get the next n characters. 199 | * 200 | * @param n The number of characters to take. 201 | * @return A string of n characters. 202 | * @throws JSONException 203 | * Substring bounds error if there are not 204 | * n characters remaining in the source string. 205 | */ 206 | public String next(int n) throws JSONException { 207 | if (n == 0) { 208 | return ""; 209 | } 210 | 211 | char[] chars = new char[n]; 212 | int pos = 0; 213 | 214 | while (pos < n) { 215 | chars[pos] = this.next(); 216 | if (this.end()) { 217 | throw this.syntaxError("Substring bounds error"); 218 | } 219 | pos += 1; 220 | } 221 | return new String(chars); 222 | } 223 | 224 | 225 | /** 226 | * Get the next char in the string, skipping whitespace. 227 | * @throws JSONException 228 | * @return A character, or 0 if there are no more characters. 229 | */ 230 | public char nextClean() throws JSONException { 231 | for (;;) { 232 | char c = this.next(); 233 | if (c == 0 || c > ' ') { 234 | return c; 235 | } 236 | } 237 | } 238 | 239 | 240 | /** 241 | * Return the characters up to the next close quote character. 242 | * Backslash processing is done. The formal JSON format does not 243 | * allow strings in single quotes, but an implementation is allowed to 244 | * accept them. 245 | * @param quote The quoting character, either 246 | * " (double quote) or 247 | * ' (single quote). 248 | * @return A String. 249 | * @throws JSONException Unterminated string. 250 | */ 251 | public String nextString(char quote) throws JSONException { 252 | char c; 253 | StringBuffer sb = new StringBuffer(); 254 | for (;;) { 255 | c = this.next(); 256 | switch (c) { 257 | case 0: 258 | case '\n': 259 | case '\r': 260 | throw this.syntaxError("Unterminated string"); 261 | case '\\': 262 | c = this.next(); 263 | switch (c) { 264 | case 'b': 265 | sb.append('\b'); 266 | break; 267 | case 't': 268 | sb.append('\t'); 269 | break; 270 | case 'n': 271 | sb.append('\n'); 272 | break; 273 | case 'f': 274 | sb.append('\f'); 275 | break; 276 | case 'r': 277 | sb.append('\r'); 278 | break; 279 | case 'u': 280 | sb.append((char)Integer.parseInt(this.next(4), 16)); 281 | break; 282 | case '"': 283 | case '\'': 284 | case '\\': 285 | case '/': 286 | sb.append(c); 287 | break; 288 | default: 289 | throw this.syntaxError("Illegal escape."); 290 | } 291 | break; 292 | default: 293 | if (c == quote) { 294 | return sb.toString(); 295 | } 296 | sb.append(c); 297 | } 298 | } 299 | } 300 | 301 | 302 | /** 303 | * Get the text up but not including the specified character or the 304 | * end of line, whichever comes first. 305 | * @param delimiter A delimiter character. 306 | * @return A string. 307 | */ 308 | public String nextTo(char delimiter) throws JSONException { 309 | StringBuffer sb = new StringBuffer(); 310 | for (;;) { 311 | char c = this.next(); 312 | if (c == delimiter || c == 0 || c == '\n' || c == '\r') { 313 | if (c != 0) { 314 | this.back(); 315 | } 316 | return sb.toString().trim(); 317 | } 318 | sb.append(c); 319 | } 320 | } 321 | 322 | 323 | /** 324 | * Get the text up but not including one of the specified delimiter 325 | * characters or the end of line, whichever comes first. 326 | * @param delimiters A set of delimiter characters. 327 | * @return A string, trimmed. 328 | */ 329 | public String nextTo(String delimiters) throws JSONException { 330 | char c; 331 | StringBuffer sb = new StringBuffer(); 332 | for (;;) { 333 | c = this.next(); 334 | if (delimiters.indexOf(c) >= 0 || c == 0 || 335 | c == '\n' || c == '\r') { 336 | if (c != 0) { 337 | this.back(); 338 | } 339 | return sb.toString().trim(); 340 | } 341 | sb.append(c); 342 | } 343 | } 344 | 345 | 346 | /** 347 | * Get the next value. The value can be a Boolean, Double, Integer, 348 | * JSONArray, JSONObject, Long, or String, or the JSONObject.NULL object. 349 | * @throws JSONException If syntax error. 350 | * 351 | * @return An object. 352 | */ 353 | public Object nextValue() throws JSONException { 354 | char c = this.nextClean(); 355 | String string; 356 | 357 | switch (c) { 358 | case '"': 359 | case '\'': 360 | return this.nextString(c); 361 | case '{': 362 | this.back(); 363 | return new JSONObject(this); 364 | case '[': 365 | this.back(); 366 | return new JSONArray(this); 367 | } 368 | 369 | /* 370 | * Handle unquoted text. This could be the values true, false, or 371 | * null, or it can be a number. An implementation (such as this one) 372 | * is allowed to also accept non-standard forms. 373 | * 374 | * Accumulate characters until we reach the end of the text or a 375 | * formatting character. 376 | */ 377 | 378 | StringBuffer sb = new StringBuffer(); 379 | while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) { 380 | sb.append(c); 381 | c = this.next(); 382 | } 383 | this.back(); 384 | 385 | string = sb.toString().trim(); 386 | if ("".equals(string)) { 387 | throw this.syntaxError("Missing value"); 388 | } 389 | return JSONObject.stringToValue(string); 390 | } 391 | 392 | 393 | /** 394 | * Skip characters until the next character is the requested character. 395 | * If the requested character is not found, no characters are skipped. 396 | * @param to A character to skip to. 397 | * @return The requested character, or zero if the requested character 398 | * is not found. 399 | */ 400 | public char skipTo(char to) throws JSONException { 401 | char c; 402 | try { 403 | long startIndex = this.index; 404 | long startCharacter = this.character; 405 | long startLine = this.line; 406 | this.reader.mark(1000000); 407 | do { 408 | c = this.next(); 409 | if (c == 0) { 410 | this.reader.reset(); 411 | this.index = startIndex; 412 | this.character = startCharacter; 413 | this.line = startLine; 414 | return c; 415 | } 416 | } while (c != to); 417 | } catch (IOException exc) { 418 | throw new JSONException(exc); 419 | } 420 | 421 | this.back(); 422 | return c; 423 | } 424 | 425 | 426 | /** 427 | * Make a JSONException to signal a syntax error. 428 | * 429 | * @param message The error message. 430 | * @return A JSONException object, suitable for throwing 431 | */ 432 | public JSONException syntaxError(String message) { 433 | return new JSONException(message + this.toString()); 434 | } 435 | 436 | 437 | /** 438 | * Make a printable string of this JSONTokener. 439 | * 440 | * @return " at {index} [character {character} line {line}]" 441 | */ 442 | public String toString() { 443 | return " at " + this.index + " [character " + this.character + " line " + 444 | this.line + "]"; 445 | } 446 | } 447 | -------------------------------------------------------------------------------- /slurp/src/main/java/org/json/JSONML.java: -------------------------------------------------------------------------------- 1 | package org.json; 2 | 3 | /* 4 | Copyright (c) 2008 JSON.org 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | The Software shall be used for Good, not Evil. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | */ 26 | 27 | import java.util.Iterator; 28 | 29 | 30 | /** 31 | * This provides static methods to convert an XML text into a JSONArray or 32 | * JSONObject, and to covert a JSONArray or JSONObject into an XML text using 33 | * the JsonML transform. 34 | * 35 | * @author JSON.org 36 | * @version 2012-03-28 37 | */ 38 | public class JSONML { 39 | 40 | /** 41 | * Parse XML values and store them in a JSONArray. 42 | * @param x The XMLTokener containing the source string. 43 | * @param arrayForm true if array form, false if object form. 44 | * @param ja The JSONArray that is containing the current tag or null 45 | * if we are at the outermost level. 46 | * @return A JSONArray if the value is the outermost tag, otherwise null. 47 | * @throws JSONException 48 | */ 49 | private static Object parse( 50 | XMLTokener x, 51 | boolean arrayForm, 52 | JSONArray ja 53 | ) throws JSONException { 54 | String attribute; 55 | char c; 56 | String closeTag = null; 57 | int i; 58 | JSONArray newja = null; 59 | JSONObject newjo = null; 60 | Object token; 61 | String tagName = null; 62 | 63 | // Test for and skip past these forms: 64 | // 65 | // 66 | // 67 | // 68 | 69 | while (true) { 70 | if (!x.more()) { 71 | throw x.syntaxError("Bad XML"); 72 | } 73 | token = x.nextContent(); 74 | if (token == XML.LT) { 75 | token = x.nextToken(); 76 | if (token instanceof Character) { 77 | if (token == XML.SLASH) { 78 | 79 | // Close tag "); 99 | } else { 100 | x.back(); 101 | } 102 | } else if (c == '[') { 103 | token = x.nextToken(); 104 | if (token.equals("CDATA") && x.next() == '[') { 105 | if (ja != null) { 106 | ja.put(x.nextCDATA()); 107 | } 108 | } else { 109 | throw x.syntaxError("Expected 'CDATA['"); 110 | } 111 | } else { 112 | i = 1; 113 | do { 114 | token = x.nextMeta(); 115 | if (token == null) { 116 | throw x.syntaxError("Missing '>' after ' 0); 123 | } 124 | } else if (token == XML.QUEST) { 125 | 126 | // "); 129 | } else { 130 | throw x.syntaxError("Misshaped tag"); 131 | } 132 | 133 | // Open tag < 134 | 135 | } else { 136 | if (!(token instanceof String)) { 137 | throw x.syntaxError("Bad tagName '" + token + "'."); 138 | } 139 | tagName = (String)token; 140 | newja = new JSONArray(); 141 | newjo = new JSONObject(); 142 | if (arrayForm) { 143 | newja.put(tagName); 144 | if (ja != null) { 145 | ja.put(newja); 146 | } 147 | } else { 148 | newjo.put("tagName", tagName); 149 | if (ja != null) { 150 | ja.put(newjo); 151 | } 152 | } 153 | token = null; 154 | for (;;) { 155 | if (token == null) { 156 | token = x.nextToken(); 157 | } 158 | if (token == null) { 159 | throw x.syntaxError("Misshaped tag"); 160 | } 161 | if (!(token instanceof String)) { 162 | break; 163 | } 164 | 165 | // attribute = value 166 | 167 | attribute = (String)token; 168 | if (!arrayForm && ("tagName".equals(attribute) || "childNode".equals(attribute))) { 169 | throw x.syntaxError("Reserved attribute."); 170 | } 171 | token = x.nextToken(); 172 | if (token == XML.EQ) { 173 | token = x.nextToken(); 174 | if (!(token instanceof String)) { 175 | throw x.syntaxError("Missing value"); 176 | } 177 | newjo.accumulate(attribute, XML.stringToValue((String)token)); 178 | token = null; 179 | } else { 180 | newjo.accumulate(attribute, ""); 181 | } 182 | } 183 | if (arrayForm && newjo.length() > 0) { 184 | newja.put(newjo); 185 | } 186 | 187 | // Empty tag <.../> 188 | 189 | if (token == XML.SLASH) { 190 | if (x.nextToken() != XML.GT) { 191 | throw x.syntaxError("Misshaped tag"); 192 | } 193 | if (ja == null) { 194 | if (arrayForm) { 195 | return newja; 196 | } else { 197 | return newjo; 198 | } 199 | } 200 | 201 | // Content, between <...> and 202 | 203 | } else { 204 | if (token != XML.GT) { 205 | throw x.syntaxError("Misshaped tag"); 206 | } 207 | closeTag = (String)parse(x, arrayForm, newja); 208 | if (closeTag != null) { 209 | if (!closeTag.equals(tagName)) { 210 | throw x.syntaxError("Mismatched '" + tagName + 211 | "' and '" + closeTag + "'"); 212 | } 213 | tagName = null; 214 | if (!arrayForm && newja.length() > 0) { 215 | newjo.put("childNodes", newja); 216 | } 217 | if (ja == null) { 218 | if (arrayForm) { 219 | return newja; 220 | } else { 221 | return newjo; 222 | } 223 | } 224 | } 225 | } 226 | } 227 | } else { 228 | if (ja != null) { 229 | ja.put(token instanceof String 230 | ? XML.stringToValue((String)token) 231 | : token); 232 | } 233 | } 234 | } 235 | } 236 | 237 | 238 | /** 239 | * Convert a well-formed (but not necessarily valid) XML string into a 240 | * JSONArray using the JsonML transform. Each XML tag is represented as 241 | * a JSONArray in which the first element is the tag name. If the tag has 242 | * attributes, then the second element will be JSONObject containing the 243 | * name/value pairs. If the tag contains children, then strings and 244 | * JSONArrays will represent the child tags. 245 | * Comments, prologs, DTDs, and <[ [ ]]> are ignored. 246 | * @param string The source string. 247 | * @return A JSONArray containing the structured data from the XML string. 248 | * @throws JSONException 249 | */ 250 | public static JSONArray toJSONArray(String string) throws JSONException { 251 | return toJSONArray(new XMLTokener(string)); 252 | } 253 | 254 | 255 | /** 256 | * Convert a well-formed (but not necessarily valid) XML string into a 257 | * JSONArray using the JsonML transform. Each XML tag is represented as 258 | * a JSONArray in which the first element is the tag name. If the tag has 259 | * attributes, then the second element will be JSONObject containing the 260 | * name/value pairs. If the tag contains children, then strings and 261 | * JSONArrays will represent the child content and tags. 262 | * Comments, prologs, DTDs, and <[ [ ]]> are ignored. 263 | * @param x An XMLTokener. 264 | * @return A JSONArray containing the structured data from the XML string. 265 | * @throws JSONException 266 | */ 267 | public static JSONArray toJSONArray(XMLTokener x) throws JSONException { 268 | return (JSONArray)parse(x, true, null); 269 | } 270 | 271 | 272 | /** 273 | * Convert a well-formed (but not necessarily valid) XML string into a 274 | * JSONObject using the JsonML transform. Each XML tag is represented as 275 | * a JSONObject with a "tagName" property. If the tag has attributes, then 276 | * the attributes will be in the JSONObject as properties. If the tag 277 | * contains children, the object will have a "childNodes" property which 278 | * will be an array of strings and JsonML JSONObjects. 279 | 280 | * Comments, prologs, DTDs, and <[ [ ]]> are ignored. 281 | * @param x An XMLTokener of the XML source text. 282 | * @return A JSONObject containing the structured data from the XML string. 283 | * @throws JSONException 284 | */ 285 | public static JSONObject toJSONObject(XMLTokener x) throws JSONException { 286 | return (JSONObject)parse(x, false, null); 287 | } 288 | 289 | 290 | /** 291 | * Convert a well-formed (but not necessarily valid) XML string into a 292 | * JSONObject using the JsonML transform. Each XML tag is represented as 293 | * a JSONObject with a "tagName" property. If the tag has attributes, then 294 | * the attributes will be in the JSONObject as properties. If the tag 295 | * contains children, the object will have a "childNodes" property which 296 | * will be an array of strings and JsonML JSONObjects. 297 | 298 | * Comments, prologs, DTDs, and <[ [ ]]> are ignored. 299 | * @param string The XML source text. 300 | * @return A JSONObject containing the structured data from the XML string. 301 | * @throws JSONException 302 | */ 303 | public static JSONObject toJSONObject(String string) throws JSONException { 304 | return toJSONObject(new XMLTokener(string)); 305 | } 306 | 307 | 308 | /** 309 | * Reverse the JSONML transformation, making an XML text from a JSONArray. 310 | * @param ja A JSONArray. 311 | * @return An XML string. 312 | * @throws JSONException 313 | */ 314 | public static String toString(JSONArray ja) throws JSONException { 315 | int i; 316 | JSONObject jo; 317 | String key; 318 | Iterator keys; 319 | int length; 320 | Object object; 321 | StringBuffer sb = new StringBuffer(); 322 | String tagName; 323 | String value; 324 | 325 | // Emit = length) { 362 | sb.append('/'); 363 | sb.append('>'); 364 | } else { 365 | sb.append('>'); 366 | do { 367 | object = ja.get(i); 368 | i += 1; 369 | if (object != null) { 370 | if (object instanceof String) { 371 | sb.append(XML.escape(object.toString())); 372 | } else if (object instanceof JSONObject) { 373 | sb.append(toString((JSONObject)object)); 374 | } else if (object instanceof JSONArray) { 375 | sb.append(toString((JSONArray)object)); 376 | } 377 | } 378 | } while (i < length); 379 | sb.append('<'); 380 | sb.append('/'); 381 | sb.append(tagName); 382 | sb.append('>'); 383 | } 384 | return sb.toString(); 385 | } 386 | 387 | /** 388 | * Reverse the JSONML transformation, making an XML text from a JSONObject. 389 | * The JSONObject must contain a "tagName" property. If it has children, 390 | * then it must have a "childNodes" property containing an array of objects. 391 | * The other properties are attributes with string values. 392 | * @param jo A JSONObject. 393 | * @return An XML string. 394 | * @throws JSONException 395 | */ 396 | public static String toString(JSONObject jo) throws JSONException { 397 | StringBuffer sb = new StringBuffer(); 398 | int i; 399 | JSONArray ja; 400 | String key; 401 | Iterator keys; 402 | int length; 403 | Object object; 404 | String tagName; 405 | String value; 406 | 407 | //Emit '); 443 | } else { 444 | sb.append('>'); 445 | length = ja.length(); 446 | for (i = 0; i < length; i += 1) { 447 | object = ja.get(i); 448 | if (object != null) { 449 | if (object instanceof String) { 450 | sb.append(XML.escape(object.toString())); 451 | } else if (object instanceof JSONObject) { 452 | sb.append(toString((JSONObject)object)); 453 | } else if (object instanceof JSONArray) { 454 | sb.append(toString((JSONArray)object)); 455 | } else { 456 | sb.append(object.toString()); 457 | } 458 | } 459 | } 460 | sb.append('<'); 461 | sb.append('/'); 462 | sb.append(tagName); 463 | sb.append('>'); 464 | } 465 | return sb.toString(); 466 | } 467 | } 468 | -------------------------------------------------------------------------------- /slurp/src/main/java/org/json/XML.java: -------------------------------------------------------------------------------- 1 | package org.json; 2 | 3 | /* 4 | Copyright (c) 2002 JSON.org 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | The Software shall be used for Good, not Evil. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | */ 26 | 27 | import java.util.Iterator; 28 | 29 | 30 | /** 31 | * This provides static methods to convert an XML text into a JSONObject, 32 | * and to covert a JSONObject into an XML text. 33 | * @author JSON.org 34 | * @version 2012-10-26 35 | */ 36 | public class XML { 37 | 38 | /** The Character '&'. */ 39 | public static final Character AMP = new Character('&'); 40 | 41 | /** The Character '''. */ 42 | public static final Character APOS = new Character('\''); 43 | 44 | /** The Character '!'. */ 45 | public static final Character BANG = new Character('!'); 46 | 47 | /** The Character '='. */ 48 | public static final Character EQ = new Character('='); 49 | 50 | /** The Character '>'. */ 51 | public static final Character GT = new Character('>'); 52 | 53 | /** The Character '<'. */ 54 | public static final Character LT = new Character('<'); 55 | 56 | /** The Character '?'. */ 57 | public static final Character QUEST = new Character('?'); 58 | 59 | /** The Character '"'. */ 60 | public static final Character QUOT = new Character('"'); 61 | 62 | /** The Character '/'. */ 63 | public static final Character SLASH = new Character('/'); 64 | 65 | /** 66 | * Replace special characters with XML escapes: 67 | *

 68 |      * & (ampersand) is replaced by &amp;
 69 |      * < (less than) is replaced by &lt;
 70 |      * > (greater than) is replaced by &gt;
 71 |      * " (double quote) is replaced by &quot;
 72 |      * 
73 | * @param string The string to be escaped. 74 | * @return The escaped string. 75 | */ 76 | public static String escape(String string) { 77 | StringBuffer sb = new StringBuffer(); 78 | for (int i = 0, length = string.length(); i < length; i++) { 79 | char c = string.charAt(i); 80 | switch (c) { 81 | case '&': 82 | sb.append("&"); 83 | break; 84 | case '<': 85 | sb.append("<"); 86 | break; 87 | case '>': 88 | sb.append(">"); 89 | break; 90 | case '"': 91 | sb.append("""); 92 | break; 93 | case '\'': 94 | sb.append("'"); 95 | break; 96 | default: 97 | sb.append(c); 98 | } 99 | } 100 | return sb.toString(); 101 | } 102 | 103 | /** 104 | * Throw an exception if the string contains whitespace. 105 | * Whitespace is not allowed in tagNames and attributes. 106 | * @param string 107 | * @throws JSONException 108 | */ 109 | public static void noSpace(String string) throws JSONException { 110 | int i, length = string.length(); 111 | if (length == 0) { 112 | throw new JSONException("Empty string."); 113 | } 114 | for (i = 0; i < length; i += 1) { 115 | if (Character.isWhitespace(string.charAt(i))) { 116 | throw new JSONException("'" + string + 117 | "' contains a space character."); 118 | } 119 | } 120 | } 121 | 122 | /** 123 | * Scan the content following the named tag, attaching it to the context. 124 | * @param x The XMLTokener containing the source string. 125 | * @param context The JSONObject that will include the new material. 126 | * @param name The tag name. 127 | * @return true if the close tag is processed. 128 | * @throws JSONException 129 | */ 130 | private static boolean parse(XMLTokener x, JSONObject context, 131 | String name) throws JSONException { 132 | char c; 133 | int i; 134 | JSONObject jsonobject = null; 135 | String string; 136 | String tagName; 137 | Object token; 138 | 139 | // Test for and skip past these forms: 140 | // 141 | // 142 | // 143 | // 144 | // Report errors for these forms: 145 | // <> 146 | // <= 147 | // << 148 | 149 | token = x.nextToken(); 150 | 151 | // "); 158 | return false; 159 | } 160 | x.back(); 161 | } else if (c == '[') { 162 | token = x.nextToken(); 163 | if ("CDATA".equals(token)) { 164 | if (x.next() == '[') { 165 | string = x.nextCDATA(); 166 | if (string.length() > 0) { 167 | context.accumulate("#text", string); 168 | } 169 | return false; 170 | } 171 | } 172 | throw x.syntaxError("Expected 'CDATA['"); 173 | } 174 | i = 1; 175 | do { 176 | token = x.nextMeta(); 177 | if (token == null) { 178 | throw x.syntaxError("Missing '>' after ' 0); 185 | return false; 186 | } else if (token == QUEST) { 187 | 188 | // "); 191 | return false; 192 | } else if (token == SLASH) { 193 | 194 | // Close tag 240 | 241 | } else if (token == SLASH) { 242 | if (x.nextToken() != GT) { 243 | throw x.syntaxError("Misshaped tag"); 244 | } 245 | if (jsonobject.length() > 0) { 246 | context.accumulate(tagName, jsonobject); 247 | } else { 248 | context.accumulate(tagName, JSONObject.NULL); 249 | } 250 | return false; 251 | 252 | // Content, between <...> and 253 | 254 | } else if (token == GT) { 255 | for (;;) { 256 | token = x.nextContent(); 257 | if (token == null) { 258 | if (tagName != null) { 259 | throw x.syntaxError("Unclosed tag " + tagName); 260 | } 261 | return false; 262 | } else if (token instanceof String) { 263 | string = (String)token; 264 | if (string.length() > 0) { 265 | jsonobject.accumulate("#text", 266 | XML.stringToValue(string)); 267 | } 268 | 269 | // Nested element 270 | 271 | } else if (token == LT) { 272 | if (parse(x, jsonobject, tagName)) { 273 | if (jsonobject.length() == 0) { 274 | context.accumulate(tagName, JSONObject.NULL); 275 | } else if (jsonobject.length() == 1 && 276 | jsonobject.opt("#text") != null) { 277 | context.accumulate(tagName, 278 | jsonobject.opt("#text")); 279 | } else { 280 | context.accumulate(tagName, jsonobject); 281 | } 282 | return false; 283 | } 284 | } 285 | } 286 | } else { 287 | throw x.syntaxError("Misshaped tag"); 288 | } 289 | } 290 | } 291 | } 292 | 293 | 294 | /** 295 | * Try to convert a string into a number, boolean, or null. If the string 296 | * can't be converted, return the string. This is much less ambitious than 297 | * JSONObject.stringToValue, especially because it does not attempt to 298 | * convert plus forms, octal forms, hex forms, or E forms lacking decimal 299 | * points. 300 | * @param string A String. 301 | * @return A simple JSON value. 302 | */ 303 | public static Object stringToValue(String string) { 304 | if ("".equals(string)) { 305 | return string; 306 | } 307 | if ("true".equalsIgnoreCase(string)) { 308 | return Boolean.TRUE; 309 | } 310 | if ("false".equalsIgnoreCase(string)) { 311 | return Boolean.FALSE; 312 | } 313 | if ("null".equalsIgnoreCase(string)) { 314 | return JSONObject.NULL; 315 | } 316 | if ("0".equals(string)) { 317 | return new Integer(0); 318 | } 319 | 320 | // If it might be a number, try converting it. If that doesn't work, 321 | // return the string. 322 | 323 | try { 324 | char initial = string.charAt(0); 325 | boolean negative = false; 326 | if (initial == '-') { 327 | initial = string.charAt(1); 328 | negative = true; 329 | } 330 | if (initial == '0' && string.charAt(negative ? 2 : 1) == '0') { 331 | return string; 332 | } 333 | if ((initial >= '0' && initial <= '9')) { 334 | if (string.indexOf('.') >= 0) { 335 | return Double.valueOf(string); 336 | } else if (string.indexOf('e') < 0 && string.indexOf('E') < 0) { 337 | Long myLong = new Long(string); 338 | if (myLong.longValue() == myLong.intValue()) { 339 | return new Integer(myLong.intValue()); 340 | } else { 341 | return myLong; 342 | } 343 | } 344 | } 345 | } catch (Exception ignore) { 346 | } 347 | return string; 348 | } 349 | 350 | 351 | /** 352 | * Convert a well-formed (but not necessarily valid) XML string into a 353 | * JSONObject. Some information may be lost in this transformation 354 | * because JSON is a data format and XML is a document format. XML uses 355 | * elements, attributes, and content text, while JSON uses unordered 356 | * collections of name/value pairs and arrays of values. JSON does not 357 | * does not like to distinguish between elements and attributes. 358 | * Sequences of similar elements are represented as JSONArrays. Content 359 | * text may be placed in a "#text" member. Comments, prologs, DTDs, and 360 | * <[ [ ]]> are ignored. 361 | * @param string The source string. 362 | * @return A JSONObject containing the structured data from the XML string. 363 | * @throws JSONException 364 | */ 365 | public static JSONObject toJSONObject(String string) throws JSONException { 366 | JSONObject jo = new JSONObject(); 367 | XMLTokener x = new XMLTokener(string); 368 | while (x.more() && x.skipPast("<")) { 369 | parse(x, jo, null); 370 | } 371 | return jo; 372 | } 373 | 374 | 375 | /** 376 | * Convert a JSONObject into a well-formed, element-normal XML string. 377 | * @param object A JSONObject. 378 | * @return A string. 379 | * @throws JSONException 380 | */ 381 | public static String toString(Object object) throws JSONException { 382 | return toString(object, null); 383 | } 384 | 385 | 386 | /** 387 | * Convert a JSONObject into a well-formed, element-normal XML string. 388 | * @param object A JSONObject. 389 | * @param tagName The optional name of the enclosing tag. 390 | * @return A string. 391 | * @throws JSONException 392 | */ 393 | public static String toString(Object object, String tagName) 394 | throws JSONException { 395 | StringBuffer sb = new StringBuffer(); 396 | int i; 397 | JSONArray ja; 398 | JSONObject jo; 399 | String key; 400 | Iterator keys; 401 | int length; 402 | String string; 403 | Object value; 404 | if (object instanceof JSONObject) { 405 | 406 | // Emit 407 | 408 | if (tagName != null) { 409 | sb.append('<'); 410 | sb.append(tagName); 411 | sb.append('>'); 412 | } 413 | 414 | // Loop thru the keys. 415 | 416 | jo = (JSONObject)object; 417 | keys = jo.keys(); 418 | while (keys.hasNext()) { 419 | key = keys.next().toString(); 420 | value = jo.opt(key); 421 | if (value == null) { 422 | value = ""; 423 | } 424 | if (value instanceof String) { 425 | string = (String)value; 426 | } else { 427 | string = null; 428 | } 429 | 430 | // Emit content in body 431 | 432 | if ("#text".equals(key)) { 433 | if (value instanceof JSONArray) { 434 | ja = (JSONArray)value; 435 | length = ja.length(); 436 | for (i = 0; i < length; i += 1) { 437 | if (i > 0) { 438 | sb.append('\n'); 439 | } 440 | sb.append(escape(ja.get(i).toString())); 441 | } 442 | } else { 443 | sb.append(escape(value.toString())); 444 | } 445 | 446 | // Emit an array of similar keys 447 | 448 | } else if (value instanceof JSONArray) { 449 | ja = (JSONArray)value; 450 | length = ja.length(); 451 | for (i = 0; i < length; i += 1) { 452 | value = ja.get(i); 453 | if (value instanceof JSONArray) { 454 | sb.append('<'); 455 | sb.append(key); 456 | sb.append('>'); 457 | sb.append(toString(value)); 458 | sb.append("'); 461 | } else { 462 | sb.append(toString(value, key)); 463 | } 464 | } 465 | } else if ("".equals(value)) { 466 | sb.append('<'); 467 | sb.append(key); 468 | sb.append("/>"); 469 | 470 | // Emit a new tag 471 | 472 | } else { 473 | sb.append(toString(value, key)); 474 | } 475 | } 476 | if (tagName != null) { 477 | 478 | // Emit the close tag 479 | 480 | sb.append("'); 483 | } 484 | return sb.toString(); 485 | 486 | // XML does not have good support for arrays. If an array appears in a place 487 | // where XML is lacking, synthesize an element. 488 | 489 | } else { 490 | if (object.getClass().isArray()) { 491 | object = new JSONArray(object); 492 | } 493 | if (object instanceof JSONArray) { 494 | ja = (JSONArray)object; 495 | length = ja.length(); 496 | for (i = 0; i < length; i += 1) { 497 | sb.append(toString(ja.opt(i), tagName == null ? "array" : tagName)); 498 | } 499 | return sb.toString(); 500 | } else { 501 | string = (object == null) ? "null" : escape(object.toString()); 502 | return (tagName == null) ? "\"" + string + "\"" : 503 | (string.length() == 0) ? "<" + tagName + "/>" : 504 | "<" + tagName + ">" + string + ""; 505 | } 506 | } 507 | } 508 | } 509 | -------------------------------------------------------------------------------- /slurp/src/main/scala/morph/ast/DSL.scala: -------------------------------------------------------------------------------- 1 | package morph.ast 2 | 3 | import morph.ast.{ ValueNode => VN } 4 | 5 | import scala.language.implicitConversions 6 | import scala.{ PartialFunction => PF } 7 | 8 | /** 9 | * A DSL for manipulating and transforming an AST. 10 | * 11 | * @author Anish Athalye 12 | */ 13 | trait DSL { 14 | 15 | /** 16 | * An implicit conversion from a type viewable as a `ValueNode` to 17 | * `Option[ValueNode]`. 18 | */ 19 | implicit def ValueNodeViewable2OptionValueNode[T <% VN]( 20 | nodeViewable: T): Option[VN] = Option(nodeViewable) 21 | 22 | /** 23 | * An implicit conversion from an option type with inner type viewable as 24 | * a `ValueNode` to `Option[ValueNode]`. 25 | */ 26 | implicit def OptionValueNodeViewable2OptionValueNode[T <% VN]( 27 | optNodeViewable: Option[T]): Option[VN] = optNodeViewable map { c => c: VN } 28 | 29 | /** 30 | * An implicit conversion from any type that can be viewed as a `ValueNode` 31 | * to an `RichOptionValueNode. 32 | * 33 | * This is necessary because Scala will never perform multiple implicit 34 | * conversions in a row. Without this, doing something like *(1) would not 35 | * work to construct an array containing a `NumberNode` because that would 36 | * require three implicit conversions `Int` to `ValueNode` to 37 | * `Option[ValueNode]` to `RichOptionValueNode`. 38 | * 39 | * Introducing another implicit conversion that can convert any type that is 40 | * viewable as a `ValueNode` into a `RichOptionValueNode` solves this 41 | * problem. This is done using a type parameter `T` that is viewable as a 42 | * `ValueNode` solves this problem. 43 | */ 44 | implicit def ValueNodeViewable2RichOptionValueNode[T <% VN]( 45 | nodeViewable: T): RichOptionValueNode = RichOptionValueNode(nodeViewable) 46 | 47 | /** 48 | * The implicit class that provides the majority of the methods in the DSL. 49 | * 50 | * These methods operate on `Option`s because methods may or may not return 51 | * results. By having these methods on `Option`s, it is very easy to chain 52 | * them and automatically filter out empty results in the end using the 53 | * ^(...) object constructor or the *(...) array constructor. 54 | */ 55 | implicit class RichOptionValueNode(opt: Option[VN]) { 56 | 57 | /** 58 | * Returns the value corresponding to the specified key in an object node. 59 | * 60 | * @example 61 | * {{{ 62 | * scala> val obj = ObjectNode("one" -> 1, "two" -> 2) 63 | * scala> val one = obj get "one" 64 | * one: Option[morph.ast.ValueNode] = Some(1) 65 | * }}} 66 | * 67 | * @param key The key corresponding to the value to retrieve. 68 | * 69 | * @return The value. 70 | */ 71 | def get(key: String): Option[VN] = opt flatMap { 72 | case ObjectNode(fields) => fields get key 73 | case _ => None 74 | } 75 | 76 | /** 77 | * Returns the value corresponding to the specified key in an ObjectNode. 78 | * 79 | * @example 80 | * {{{ 81 | * scala> val obj = ObjectNode("one" -> 1, "two" -> 2) 82 | * scala> val one = obj ~> "one" 83 | * one: Option[morph.ast.ValueNode] = Some(1) 84 | * }}} 85 | * 86 | * @param key The key corresponding to the value to retrieve. 87 | * 88 | * @return The value. 89 | */ 90 | def ~>(key: String): Option[VN] = opt get key 91 | 92 | /** 93 | * Returns the element corresponding to the specified index in an array 94 | * node. 95 | * 96 | * @example 97 | * {{{ 98 | * scala> val arr = ArrayNode("zero", "one", "two", "three") 99 | * scala> val first = arr get 1 100 | * first: Option[morph.ast.ValueNode] = Some("one") 101 | * }}} 102 | * 103 | * @note This method uses 0-based indexing. 104 | * 105 | * @param index The index of the element to retrieve. 106 | * 107 | * @return The element. 108 | */ 109 | def get(index: Int): Option[VN] = opt flatMap { 110 | case ArrayNode(elem) => elem lift index 111 | case _ => None 112 | } 113 | 114 | /** 115 | * Returns the element corresponding to the specified index in an array 116 | * node. 117 | * 118 | * @example 119 | * {{{ 120 | * scala> val arr = ArrayNode("one", "two", "three") 121 | * scala> val first = arr ~> 1 122 | * first: Option[morph.ast.ValueNode] = Some("one") 123 | * }}} 124 | * 125 | * @note This method uses 1-based indexing. 126 | * 127 | * @param index The index of the element to retrieve. 128 | * 129 | * @return The element. 130 | */ 131 | def ~>(index: Int): Option[VN] = opt get index 132 | 133 | /** 134 | * Recursively searches for a value corresponding to the specified key. 135 | * 136 | * @example 137 | * 138 | * {{{ 139 | * scala> val inner = ObjectNode("test" -> 1, "more" -> 2) 140 | * scala> val arr = ArrayNode(inner, ObjectNode("test" -> 2, "inner" -> inner)) 141 | * scala> val test = arr recGet "test" 142 | * one: Option[morph.ast.ValueNode] = 143 | * Some([ 144 | * 1, 145 | * 2, 146 | * 1 147 | * ]) 148 | * }}} 149 | * 150 | * @note This method does a depth first traversal and is '''not''' 151 | * tail recursive. 152 | * 153 | * @param key The key corresponding to the values to retrieve. 154 | * 155 | * @return A list of all matching nodes. 156 | */ 157 | def recGet(key: String): Option[ArrayNode] = { 158 | def iter(node: VN, key: String): IndexedSeq[VN] = node match { 159 | case ObjectNode(fields) => { 160 | val sub = fields.toVector map { 161 | case (k, v) => iter(v, key) 162 | } 163 | fields get key match { 164 | case Some(value) => value +: sub.flatten 165 | case None => sub.flatten 166 | } 167 | } 168 | case ArrayNode(elem) => { 169 | val sub = elem map { iter(_, key) } 170 | sub.flatten 171 | } 172 | case _ => Vector() 173 | } 174 | opt map { node => ArrayNode(iter(node, key)) } 175 | } 176 | 177 | /** 178 | * Recursively searches for a value corresponding to the specified key. 179 | * 180 | * @example 181 | * 182 | * {{{ 183 | * scala> val inner = ObjectNode("test" -> 1, "more" -> 2) 184 | * scala> val arr = ArrayNode(inner, ObjectNode("test" -> 2, "inner" -> inner)) 185 | * scala> val test = arr ~>> "test" 186 | * one: Option[morph.ast.ValueNode] = 187 | * Some([ 188 | * 1, 189 | * 2, 190 | * 1 191 | * ]) 192 | * }}} 193 | * 194 | * @note This method does a depth first traversal and is '''not''' 195 | * tail recursive. 196 | * 197 | * @param key The key corresponding to the values to retrieve. 198 | * 199 | * @return A list of all matching nodes. 200 | */ 201 | def ~>>(key: String): Option[ArrayNode] = opt recGet key 202 | 203 | /** 204 | * Maps a function over an array node. 205 | * 206 | * The function can return an `Option[ValueNode]`, or it can return 207 | * a `ValueNode` in which case the return value will be implicitly 208 | * converted to an `Option[ValueNode]`. 209 | * 210 | * @param func The function to map. 211 | * 212 | * @return An array with the function applied to each element. 213 | */ 214 | def mapFunc(func: VN => Option[VN]): Option[VN] = 215 | opt mapPartial Function.unlift(func) 216 | 217 | /** 218 | * Maps a function over an array node. 219 | * 220 | * The function can return an `Option[ValueNode]`, or it can return 221 | * a `ValueNode` in which case the return value will be implicitly 222 | * converted to an `Option[ValueNode]`. 223 | * 224 | * @param func The function to map. 225 | * 226 | * @return An array with the function applied to each element. 227 | */ 228 | def %->(func: VN => Option[VN]): Option[VN] = opt mapFunc func 229 | 230 | /** 231 | * Maps a partial function over an array node. 232 | * 233 | * @param func The partial function to map. 234 | * 235 | * @return An array with the function applied (where applicable) 236 | * to each element. 237 | */ 238 | def mapPartial(func: PF[VN, VN]): Option[VN] = opt collect { 239 | case ArrayNode(elem) => ArrayNode(elem collect func) 240 | } 241 | 242 | /** 243 | * Maps a partial function over an array node. 244 | * 245 | * @param func The partial function to map. 246 | * 247 | * @return An array with the function applied (where applicable) 248 | * to each element. 249 | */ 250 | def %~>(func: PF[VN, VN]): Option[VN] = opt mapPartial func 251 | 252 | /** 253 | * Applies a function to a value node or maps the function over the 254 | * elements of an array node. 255 | * 256 | * This is useful for dealing with ambiguities between a single element 257 | * and an array of elements. 258 | * 259 | * @note This method should not be used when there is an ambiguity 260 | * between an array of arrays and a single array. 261 | * 262 | * @param func The function to apply or map. 263 | * 264 | * @return Either an object with the function applied or an array 265 | * with the function applied to each element. 266 | */ 267 | def applyOrMapFunc(func: VN => Option[VN]): Option[VN] = opt flatMap { 268 | case arr: ArrayNode => arr mapFunc func 269 | case other => func(other) 270 | } 271 | 272 | /** 273 | * Applies a function to a value node or maps the function over the 274 | * elements of an array node. 275 | * 276 | * This is useful for dealing with ambiguities between a single element 277 | * and an array of elements. 278 | * 279 | * @note This method should not be used when there is an ambiguity 280 | * between an array of arrays and a single array. 281 | * 282 | * @param func The function to apply or map. 283 | * 284 | * @return Either an object with the function applied or an array 285 | * with the function applied to each element. 286 | */ 287 | def %%->(func: VN => Option[VN]): Option[VN] = opt applyOrMapFunc func 288 | 289 | /** 290 | * Applies a partial function to a value node or maps the partial 291 | * function over the elements of an array node. 292 | * 293 | * This is useful for dealing with ambiguities between a single element 294 | * and an array of elements. 295 | * 296 | * @note This method should not be used when there is an ambiguity 297 | * between an array of arrays and a single array. 298 | * 299 | * @param func The partial function to apply or map. 300 | * 301 | * @return Either an object with the function applied (if applicable) 302 | * or an array with the function (where applicable) applied to each element. 303 | */ 304 | def applyOrMapPartial(func: PF[VN, VN]): Option[VN] = opt flatMap { 305 | case arr: ArrayNode => arr mapPartial func 306 | case other => func.lift(other) 307 | } 308 | 309 | /** 310 | * Applies a partial function to a value node or maps the partial 311 | * function over the elements of an array node. 312 | * 313 | * This is useful for dealing with ambiguities between a single element 314 | * and an array of elements. 315 | * 316 | * @note This method should not be used when there is an ambiguity 317 | * between an array of arrays and a single array. 318 | * 319 | * @param func The partial function to apply or map. 320 | * 321 | * @return Either an object with the function applied (if applicable) 322 | * or an array with the function (where applicable) applied to each element. 323 | */ 324 | def %%~>(func: PF[VN, VN]): Option[VN] = 325 | opt applyOrMapPartial func 326 | 327 | /** 328 | * Filters an array node by a predicate. 329 | * 330 | * @example 331 | * {{{ 332 | * scala> val arr = ArrayNode(1, 2, 3, 4, 5, 6, 7, "eight") 333 | * scala> val even = arr applyFilter { 334 | * | case NumberNode(value) if value % 2 == 0 => true 335 | * | case _ => false 336 | * | } 337 | * even: Option[morph.ast.ValueNode] = 338 | * Some([ 339 | * 2, 340 | * 4, 341 | * 6 342 | * ]) 343 | * }}} 344 | * 345 | * @param pred The predicate to filter by. 346 | * 347 | * @return An array containing only the nodes that satisfy the predicate. 348 | */ 349 | def applyFilter(pred: VN => Boolean): Option[VN] = opt collect { 350 | case ArrayNode(elem) => ArrayNode(elem filter pred) 351 | } 352 | 353 | /** 354 | * Encapsulates a node in an array if the node is not already an array. 355 | * 356 | * @example 357 | * {{{ 358 | * scala> val arr = (1).encapsulate 359 | * arr: Option[morph.ast.ValueNode] = 360 | * Some([ 361 | * 1 362 | * ]) 363 | * }}} 364 | * 365 | * @return An array containing the single node, or the node itself if it 366 | * was already an array. 367 | */ 368 | def encapsulate: Option[VN] = opt map { 369 | case arr: ArrayNode => arr 370 | case other => ArrayNode(other) 371 | } 372 | 373 | /** 374 | * Flattens an array of arrays. 375 | * 376 | * @example 377 | * {{{ 378 | * scala> val arr = ArrayNode(ArrayNode(1, 2), ArrayNode(3), ArrayNode()) 379 | * scala> val flattened = arr.applyFlatten 380 | * flattened: Option[morph.ast.ValueNode] = 381 | * Some([ 382 | * 1, 383 | * 2, 384 | * 3 385 | * ]) 386 | * }}} 387 | * 388 | * @return An array that contains all the elements contained within inner 389 | * arrays. 390 | */ 391 | def applyFlatten: Option[VN] = opt collect { 392 | case ArrayNode(elem) => ArrayNode(elem flatMap { 393 | case ArrayNode(inner) => inner 394 | case _ => List() 395 | }) 396 | } 397 | 398 | /** 399 | * Flattens an array of elements that may or may not be arrays. 400 | * 401 | * If the node is a single element (that is not an array), the node 402 | * is returned as is. 403 | * 404 | * @example 405 | * {{{ 406 | * scala> val arr = ArrayNode(ArrayNode(1, 2), 3) 407 | * scala> val flattened = arr.autoFlatten 408 | * flattened: Option[morph.ast.ValueNode] = 409 | * Some([ 410 | * 1, 411 | * 2, 412 | * 3 413 | * ]) 414 | * }}} 415 | * 416 | * @return An array that contains all the elements contained within inner 417 | * arrays (flattened if the inner element is an array). 418 | */ 419 | def autoFlatten: Option[VN] = opt map { 420 | case ArrayNode(elem) => ArrayNode(elem flatMap { 421 | case ArrayNode(inner) => inner 422 | case other => List(other) 423 | }) 424 | case other => other 425 | } 426 | 427 | /** 428 | * Recursively flattens arrays (without recursively examining objects). 429 | * 430 | * If the node is a single element (that is not an array), the node 431 | * is returned as is. 432 | * 433 | * @example 434 | * {{{ 435 | * scala> val arr = ArrayNode(ArrayNode(1, ArrayNode(2)), ArrayNode(3)) 436 | * scala> val flattened = arr.autoFlatten 437 | * flattened: Option[morph.ast.ValueNode] = 438 | * Some([ 439 | * 1, 440 | * 2, 441 | * 3 442 | * ]) 443 | * }}} 444 | * 445 | * @return An array that contains all the elements contained within inner 446 | * elements (that are possibly arrays). 447 | */ 448 | def recFlatten: Option[VN] = opt map { 449 | case ArrayNode(elem) => ArrayNode(elem flatMap { 450 | case arr: ArrayNode => arr.recFlatten.asVector 451 | case other => List(other) 452 | }) 453 | case other => other 454 | } 455 | 456 | /** 457 | * Returns true if and only if the node is empty. 458 | * 459 | * This is defined differently for the different node types. An `Option` 460 | * with the value `None` is considered to be empty. An `ObjectNode` with 461 | * no key/value pairs is empty. An `ArrayNode` with no elements is empty. 462 | * A `StringNode` with an empty string is empty. `BooleanNode`s are 463 | * nonempty, and `NullNode` is empty. 464 | * 465 | * @return `true` if the node is empty. 466 | */ 467 | def nodeEmpty: Boolean = opt map { 468 | case ObjectNode(fields) => fields.isEmpty 469 | case ArrayNode(elem) => elem.isEmpty 470 | case StringNode(s) => s.isEmpty 471 | case NullNode => true 472 | case _ => false 473 | } getOrElse true 474 | 475 | /** 476 | * Returns true if the node is nonempty. 477 | * 478 | * This is defined differently for the different node types. An `Option` 479 | * with the value `None` is considered to be empty. An `ObjectNode` with 480 | * no key/value pairs is empty. An `ArrayNode` with no elements is empty. 481 | * A `StringNode` with an empty string is empty. `BooleanNode`s are 482 | * nonempty, and `NullNode` is empty. 483 | * 484 | * @return `true` if the node is not empty. 485 | */ 486 | def nodeNonEmpty: Boolean = !nodeEmpty 487 | 488 | // Unsafe operations 489 | 490 | /** 491 | * Returns true if the node is an `ObjectNode`. 492 | * 493 | * @return `true` if the node is an `ObjectNode`. 494 | */ 495 | def isObject: Boolean = opt map { 496 | _.isInstanceOf[ObjectNode] 497 | } getOrElse false 498 | 499 | /** 500 | * Converts the node to an `ObjectNode`. 501 | * 502 | * @throws NodeExtractionException If the node is empty or the node 503 | * is not an `ObjectNode`. 504 | * 505 | * @return The node as an `ObjectNode`. 506 | */ 507 | def asObjectNode: ObjectNode = opt map { 508 | case obj: ObjectNode => obj 509 | case _ => throw NodeExtractionException( 510 | "node is not an ObjectNode" 511 | ) 512 | } getOrElse { 513 | throw NodeExtractionException("node is empty") 514 | } 515 | 516 | /** 517 | * Converts the node to an `ObjectNode` and extracts its fields. 518 | * 519 | * @throws NodeExtractionException If the node is empty or the node 520 | * is not an `ObjectNode`. 521 | * 522 | * @return The node as a `Map[String, ValueNode]`. 523 | */ 524 | def asMap: Map[String, VN] = asObjectNode.fields 525 | /** 526 | * Returns true if the node is an `ArrayNode`. 527 | * 528 | * @return `true` if the node is an `ObjectNode`. 529 | */ 530 | def isArray: Boolean = opt map { 531 | _.isInstanceOf[ArrayNode] 532 | } getOrElse false 533 | 534 | /** 535 | * Converts the node to an `ArrayNode`. 536 | * 537 | * @throws NodeExtractionException If the node is empty or the node 538 | * is not an `ArrayNode`. 539 | * 540 | * @return The node as an `ArrayNode`. 541 | */ 542 | def asArrayNode: ArrayNode = opt map { 543 | case arr: ArrayNode => arr 544 | case _ => throw NodeExtractionException( 545 | "node is not an ArrayNode" 546 | ) 547 | } getOrElse { 548 | throw NodeExtractionException("node is empty") 549 | } 550 | 551 | /** 552 | * Converts the node to an `ArrayNode` and extracts its elements. 553 | * 554 | * @throws NodeExtractionException If the node is empty or the node 555 | * is not an `ArrayNode`. 556 | * 557 | * @return The node as a `IndexedSeq[ValueNode]`. 558 | */ 559 | def asIndexedSeq: IndexedSeq[VN] = asArrayNode.elements 560 | /** 561 | * Converts the node to an `ArrayNode` and extracts its elements. 562 | * 563 | * @throws NodeExtractionException If the node is empty or the node 564 | * is not an `ArrayNode`. 565 | * 566 | * @return The node as a `Vector[ValueNode]`. 567 | */ 568 | def asVector: Vector[VN] = asIndexedSeq.toVector 569 | /** 570 | * Converts the node to an `ArrayNode` and extracts its elements. 571 | * 572 | * @throws NodeExtractionException If the node is empty or the node 573 | * is not an `ArrayNode`. 574 | * 575 | * @return The node as a `List[ValueNode]`. 576 | */ 577 | def asList: List[VN] = asVector.toList 578 | /** 579 | * Returns true if the node is a `StringNode`. 580 | * 581 | * @return `true` if the node is a `StringNode`. 582 | */ 583 | def isString: Boolean = opt map { 584 | _.isInstanceOf[StringNode] 585 | } getOrElse false 586 | 587 | /** 588 | * Converts the node to a `StringNode`. 589 | * 590 | * @throws NodeExtractionException If the node is empty or the node 591 | * is not a `StringNode`. 592 | * 593 | * @return The node as an `ArrayNode`. 594 | */ 595 | def asStringNode: StringNode = opt map { 596 | case sn: StringNode => sn 597 | case _ => throw NodeExtractionException( 598 | "node is not a StringNode" 599 | ) 600 | } getOrElse { 601 | throw NodeExtractionException("node is empty") 602 | } 603 | 604 | /** 605 | * Converts the node to a `StringNode` and extracts the string it contains. 606 | * 607 | * @throws NodeExtractionException If the node is empty or the node 608 | * is not a `StringNode`. 609 | * 610 | * @return The node as a `String`. 611 | */ 612 | def asString: String = opt map { 613 | case StringNode(str) => str 614 | case _ => throw NodeExtractionException( 615 | "node is not a StringNode" 616 | ) 617 | } getOrElse { 618 | throw NodeExtractionException("node is empty") 619 | } 620 | 621 | /** 622 | * Returns true if the node is a `NumberNode`. 623 | * 624 | * @return `true` if the node is a `NumberNode`. 625 | */ 626 | def isNumber: Boolean = opt map { 627 | _.isInstanceOf[NumberNode] 628 | } getOrElse false 629 | 630 | /** 631 | * Converts the node to a `NumberNode`. 632 | * 633 | * @throws NodeExtractionException If the node is empty or the node 634 | * is not a `NumberNode`. 635 | * 636 | * @return The node as a `NumberNode`. 637 | */ 638 | def asNumberNode: NumberNode = opt map { 639 | case nn: NumberNode => nn 640 | case _ => throw NodeExtractionException( 641 | "node is not a NumberNode" 642 | ) 643 | } getOrElse { 644 | throw NodeExtractionException("node is empty") 645 | } 646 | 647 | /** 648 | * Converts the node to a `NumberNode` and extracts its value. 649 | * 650 | * @throws NodeExtractionException If the node is empty or the node 651 | * is not a `NumberNode`. 652 | * 653 | * @return The node as a `BigDecimal`. 654 | */ 655 | def asNumber: BigDecimal = asNumberNode.value 656 | 657 | /** 658 | * Converts the node to a `NumberNode` and extracts its value. 659 | * 660 | * @throws NodeExtractionException If the node is empty or the node 661 | * is not a `NumberNode`. 662 | * 663 | * @return The node as a `BigDecimal`. 664 | */ 665 | def asBigDecimal: BigDecimal = asNumber 666 | 667 | /** 668 | * Returns true if the node is a `BooleanNode`. 669 | * 670 | * @return `true` if the node is a `BooleanNode`. 671 | */ 672 | def isBoolean: Boolean = opt map { 673 | _.isInstanceOf[BooleanNode] 674 | } getOrElse false 675 | 676 | /** 677 | * Converts the node to a `BooleanNode`. 678 | * 679 | * @throws NodeExtractionException If the node is empty or the node 680 | * is not a `BooleanNode`. 681 | * 682 | * @return The node as a `BooleanNode`. 683 | */ 684 | def asBooleanNode: BooleanNode = opt map { 685 | case bn: BooleanNode => bn 686 | case _ => throw NodeExtractionException( 687 | "node is not a BooleanNode" 688 | ) 689 | } getOrElse { 690 | throw new NodeExtractionException("node is empty") 691 | } 692 | 693 | /** 694 | * Converts the node to a `BooleanNode` and extracts its value. 695 | * 696 | * @throws NodeExtractionException If the node is empty or the node 697 | * is not a `BooleanNode`. 698 | * 699 | * @return The node as a `Boolean`. 700 | */ 701 | def asBoolean: Boolean = asBooleanNode.value 702 | } 703 | 704 | /** 705 | * An object that easily constructs `ObjectNode`s. 706 | * 707 | * This can construct `ObjectNode`s from `(String, Option[ValueNode])*`. 708 | * Empty values (`None`s) are automatically filtered out. This makes 709 | * it very easy to chain computations and then filter out the results 710 | * of unsuccessful ones. 711 | * 712 | * This constructor provides very concise syntax and flexibility, 713 | * and it is completely type safe. 714 | * 715 | * @example 716 | * {{{ 717 | * scala> val obj = ^("one" -> 1, "true" -> true, 718 | * | "false" -> FalseNode, "null" -> NullNode, 719 | * | "test" -> "yes", "opt" -> Option(3)) 720 | * obj: morph.ast.ObjectNode = 721 | * { 722 | * "one": 1, 723 | * "true": true, 724 | * "false": false, 725 | * "null": null, 726 | * "test": "yes", 727 | * "opt": 3 728 | * } 729 | * }}} 730 | * 731 | */ 732 | object ^ { 733 | 734 | /** 735 | * Constructs an `ObjectNode`. 736 | * 737 | * @param fields The mappings to use to create an `ObjectNode`. 738 | * 739 | * @return An `ObjectNode` with the given mappings. 740 | */ 741 | def apply(fields: (String, Option[VN])*): ObjectNode = { 742 | val flattened = fields collect { case (k, Some(v)) => k -> v } 743 | ObjectNode(flattened: _*) 744 | } 745 | } 746 | 747 | /** 748 | * An object that easily constructs `ArrayNode`s. 749 | * 750 | * This can construct `ArrayNode`s from `Option[ValueNode]*`. Empty 751 | * elements (`None`s) are automatically filtered out. This makes 752 | * it very easy to chain computations and then filter out the results 753 | * of unsuccessful ones. 754 | * 755 | * This constructor provides very concise syntax and flexibility, 756 | * and it is completely type safe. 757 | * 758 | * @example 759 | * {{{ 760 | * scala> *(1, "two", Option(3), None, true, ^("test" -> true)) 761 | * res3: morph.ast.ArrayNode = 762 | * [ 763 | * 1, 764 | * "two", 765 | * 3, 766 | * true, 767 | * { 768 | * "test": true 769 | * } 770 | * ] 771 | * }}} 772 | */ 773 | object * { 774 | 775 | /** 776 | * Constructs an `ArrayNode`. 777 | * 778 | * @param elements The elements to use to create an `ArrayNode`. 779 | * 780 | * @return An `ArrayNode` with the given elements. 781 | */ 782 | def apply(elements: Option[VN]*): ArrayNode = 783 | ArrayNode(elements.flatten: _*) 784 | } 785 | 786 | /** 787 | * Safely compute by catching `NodeExtractionException` 788 | * and returning None if that exception occurs. 789 | * 790 | * This method can be used like a block to safely perform computations 791 | * on values contained within `Option[ValueNode]`s. This method catches 792 | * `NodeExtractionException`s caused by the `as[type]` methods and returns 793 | * `None` if an exception is thrown. If a computation is successful, the 794 | * method returns the result wrapped in an `Option`. 795 | * 796 | * @example 797 | * {{{ 798 | * scala> val arr = *(1, "two", 3, "four", 5, true) 799 | * scala> val doubled = arr mapFunc { elem => 800 | * | Safely { 801 | * | (elem.asNumber * 2).toString + "!" 802 | * | } 803 | * | } 804 | * doubled: Option[morph.ast.ValueNode] = 805 | * Some([ 806 | * "2!", 807 | * "6!", 808 | * "10!" 809 | * ]) 810 | * }}} 811 | * 812 | * @param computation The computation to perform. 813 | * 814 | * @return The result of the computation wrapped in an `Option` 815 | * (if successful, otherwise `None`). 816 | */ 817 | def Safely[T <% Option[VN]](computation: => T): Option[VN] = { 818 | try { 819 | computation 820 | } catch { 821 | case e: NodeExtractionException => None 822 | } 823 | } 824 | 825 | /** 826 | * An exception that is thrown when there is an error extracting a node. 827 | * 828 | * This may occur when attempting to get a `ValueNode` from an 829 | * `Option[ValueNode]` when it is empty. 830 | * 831 | * This may also occur when illegally converting a `ValueNode` to one 832 | * of its subtypes. 833 | */ 834 | class NodeExtractionException( 835 | message: String = null, 836 | cause: Throwable = null) 837 | extends RuntimeException(message, cause) 838 | 839 | object NodeExtractionException { 840 | 841 | def apply() = new NodeExtractionException(null, null) 842 | 843 | def apply(msg: String) = new NodeExtractionException(msg, null) 844 | 845 | def apply(msg: String, cause: Throwable) = 846 | new NodeExtractionException(msg, cause) 847 | 848 | def apply(cause: Throwable) = new NodeExtractionException(null, cause) 849 | } 850 | } 851 | 852 | /** 853 | * A companion object that extends `DSL` so that DSL methods 854 | * may be imported into scope instead of using the DSL as a mixin. 855 | */ 856 | object DSL extends DSL 857 | --------------------------------------------------------------------------------