Browse Source

first java commit

master
Thomas Withiam 3 years ago
parent
commit
75ce47d62f
  1. 90
      .gitignore
  2. 14
      configs.json
  3. 6
      src/edu/clarkson/cosi/wishlist/WebSys/IWebSys.java
  4. 152
      src/edu/clarkson/cosi/wishlist/WebSys/WebSys.java
  5. 36
      src/edu/clarkson/cosi/wishlist/core/Action.java
  6. 99
      src/edu/clarkson/cosi/wishlist/core/AddressMatcher.java
  7. 62
      src/edu/clarkson/cosi/wishlist/core/Main.java
  8. 99
      src/edu/clarkson/cosi/wishlist/core/Util.java
  9. 139
      src/edu/clarkson/cosi/wishlist/database/Database.java
  10. 12
      src/edu/clarkson/cosi/wishlist/database/IDBAction.java
  11. 12
      src/edu/clarkson/cosi/wishlist/database/IDatabase.java
  12. 18
      src/net/pop1040/JSON/JSON.java
  13. 175
      src/net/pop1040/JSON/JSONArray.java
  14. 153
      src/net/pop1040/JSON/JSONObject.java
  15. 271
      src/net/pop1040/JSON/JSONValue.java

90
.gitignore

@ -1,82 +1,10 @@
# ---> Eclipse
.metadata
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.settings/
.loadpath
.recommenders
# Eclipse Core
.project
# External tool builders
.externalToolBuilders/
# Locally stored "Eclipse launch configurations"
*.launch
# PyDev specific (Python IDE for Eclipse)
*.pydevproject
# CDT-specific (C/C++ Development Tooling)
.cproject
# JDT-specific (Eclipse Java Development Tools)
.classpath
# Java annotation processor (APT)
.factorypath
# PDT-specific (PHP Development Tools)
.buildpath
# sbteclipse plugin
.target
# Tern plugin
.tern-project
# TeXlipse plugin
.texlipse
# STS (Spring Tool Suite)
.springBeans
# Code Recommenders
.recommenders/
# Scala IDE specific (Scala & Java development for Eclipse)
.cache-main
.scala_dependencies
.worksheet
# ---> Java
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
/.settings/
/.classpath
/.project
/bin/
/database/
/Wishlist.iml
/.idea/
/out/
.DS_Store

14
configs.json

@ -0,0 +1,14 @@
{
"database":{
"databaseDirectory":"database/"
},
"webSystem":{
"port": 8027
},
"allowedIPRanges":[
{
"IP":"localhost",
"Mask":"/22"
}
]
}

6
src/edu/clarkson/cosi/wishlist/WebSys/IWebSys.java

@ -0,0 +1,6 @@
package edu.clarkson.cosi.wishlist.WebSys;
public interface IWebSys {
void start();
}

152
src/edu/clarkson/cosi/wishlist/WebSys/WebSys.java

@ -0,0 +1,152 @@
package edu.clarkson.cosi.wishlist.WebSys;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import edu.clarkson.cosi.wishlist.core.Action;
import edu.clarkson.cosi.wishlist.core.Main;
import net.pop1040.JSON.JSON;
import net.pop1040.JSON.JSONObject;
import net.pop1040.JSON.JSONValue;
import java.io.*;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
public class WebSys implements IWebSys {
final int port;
final int DEFAULT_PORT = 8027;
public WebSys(JSONObject configs) {
if (configs.isLong("port")) {
port = (int) configs.getLong("port");
} else if (configs.exists("port")) {
System.err.println("The field \"port\" exists in the webSystem portion of the config file, but it is not an integer (whole number), defaulting to " + DEFAULT_PORT);
if(configs.isString("port"))try{
Integer.parseInt(configs.getString("port"));
System.err.println("(It looks like the field \"port\" was set to a string (text in quotes) that contains an integer, did you accidentally add quotes?)");
}catch(NumberFormatException e) {}
port = DEFAULT_PORT;
} else {
System.out.println("The integer (whole number) \"port\" was not specified in the webSystem portion of the config file, defaulting to " + DEFAULT_PORT);
port = DEFAULT_PORT;
}
}
@Override
public void start() {
try {
HttpServer serverSocket = HttpServer.create(new InetSocketAddress(port), 0);
serverSocket.createContext("/", (HttpExchange httpExchange) -> {
// Begin HTTP Exchange
InputStream is = httpExchange.getRequestBody();
StringBuilder sb = new StringBuilder();
// Read input
new BufferedReader(new InputStreamReader(is));
int c;
while ((c = is.read()) != -1) {
sb.append((char) c);
}
JSONObject output = null;
System.out.println("Input:[" + sb.toString() + "]");
// Parse input
JSONValue jsonpacket = JSON.parse(sb.toString());
if (jsonpacket == null) {
output = new JSONObject().setBoolean("error", true);
output.setString("error", "Unable to parse JSON packet.").setString("errorType", "Root error");
} else {
// Check if JSON packet is an object
if (!(jsonpacket instanceof JSONObject)) {
output = new JSONObject().setBoolean("error", true);
output.setString("error", "JSON packet was not an object.").setString("errorType", "Root error");
} else {
JSONObject input = (JSONObject) jsonpacket;
// Determine which action was performed
String actionName = input.isString("action") ? input.getString("action") : null;
System.out.println("Action: " + actionName + ", json: " + input.toString());
Action action = Main.actionLookup(actionName);
if (action == null) output = new JSONObject().setString("error", "unknown action, are you probing for commands?").setString("errorType", "Root error");
else {
try {
action.setRequesterAddress(httpExchange.getRemoteAddress());
action.setJson(input);
output = Main.processAction(action);
} catch (Exception e) {
output = new JSONObject().setString("error", "Internal server error, database errored").setString("errorType", "Internal error");
e.printStackTrace();
}
}
// if (actionClass == null) {
//
// output = new JSONObject().setBoolean("error", true);
// output.setString("text", "Action does not exist.");
// } else {
// Constructor<?> cons;
// try {
//
// //Create and perform generic action
// cons = actionClass.getConstructor(JSONObject.class);
// Action action = (Action) cons.newInstance(input);
// output = Main.processAction(action);
// } catch (NoSuchMethodException e) {
//
// e.printStackTrace();
// } catch (IllegalAccessException e) {
//
// e.printStackTrace();
// } catch (InstantiationException e) {
//
// e.printStackTrace();
// } catch (InvocationTargetException e) {
//
// e.printStackTrace();
// } finally {
// if (output == null) {
//
// output = new JSONObject().setBoolean("error", true);
// output.setString("text", "Internal server error.");
// }
// }
// }
}
}
// Send data
String response = output.toString();
httpExchange.getResponseHeaders().add("Content-Type", "application/json");
httpExchange.sendResponseHeaders(200, output.toString().length()+1);
OutputStreamWriter sw = new OutputStreamWriter(httpExchange.getResponseBody());
sw.write(response);
sw.write('\n');
sw.flush();
sw.close();
});
serverSocket.setExecutor(Executors.newCachedThreadPool()); // because isn't supporting more than one user at
// a time nice?
serverSocket.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}

36
src/edu/clarkson/cosi/wishlist/core/Action.java

@ -0,0 +1,36 @@
package edu.clarkson.cosi.wishlist.core;
import java.net.InetSocketAddress;
import net.pop1040.JSON.JSONObject;
public class Action {
protected String actionName;
protected InetSocketAddress remoteAddress;
protected JSONObject json;
public Action(String actionName) {
this.actionName = actionName;
}
public String getActionName() {
return actionName;
}
public void setRequesterAddress(InetSocketAddress remoteAddress) {
this.remoteAddress = remoteAddress;
}
public InetSocketAddress getRemoteAddress() {
return remoteAddress;
}
public void setJson(JSONObject json) {
this.json = json;
}
public JSONObject getJson() {
return json;
}
}

99
src/edu/clarkson/cosi/wishlist/core/AddressMatcher.java

@ -0,0 +1,99 @@
package edu.clarkson.cosi.wishlist.core;
import java.net.DatagramSocket;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
public class AddressMatcher {
private static class Range{
byte[] ip;
byte[] mask;
public Range(byte[] ip, byte[] mask) {
this.ip = ip;
this.mask = mask;
System.out.println((0xFF & ip [0]) + "." + (0xFF & ip [1]) + "." + (0xFF & ip [2]) + "." + (0xFF & ip [3]));
System.out.println((0xFF & mask[0]) + "." + (0xFF & mask[1]) + "." + (0xFF & mask[2]) + "." + (0xFF & mask[3]));
}
boolean matches(Inet4Address address) {
for(int i=0; i<4; i++)if((address.getAddress()[i] & mask[i]) != (ip[i] & mask[i]))return false;
return true;
}
}
ArrayList<Range> ranges = new ArrayList<Range>();
public boolean inRange(Inet4Address address) {
for(Range r : ranges) if(r.matches(address))return true;
return false;
}
public void addRange(String ip, String mask) {
byte[] ipVal = null;
byte[] maskVal = null;
if("localhost".equalsIgnoreCase(ip)) {
try(final DatagramSocket socket = new DatagramSocket()){ //get preferred out bound IP address, 1.1.1.1 does not need to be reachable
socket.connect(InetAddress.getByName("1.1.1.1"), 10002);
ipVal = socket.getLocalAddress().getAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (SocketException e1) {
e1.printStackTrace();
}
if(ipVal == null) { //If the IP is IPv6 or fails for another reason, fall back to normal local host
try {
ipVal = Inet4Address.getLocalHost().getAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
System.err.println("Unable to determine localhost, something is very wrong");
System.exit(2);
}
}
}else { //if its not localhost, just parse it normally
try {
ipVal = Inet4Address.getByName(ip).getAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
System.err.println("Unable to parse IP address \"" + ip + "\"");
System.exit(2);
}
}
if(mask != null && mask.startsWith("/")) {
try {
int amount = Integer.parseInt(mask.substring(1));
if(amount < 0) {
System.err.println("Mask cannot be less than 0, that doesn't make any sense, 0 selects all IPs, how could you have more than that");
System.exit(1);
}
if(amount > 32) {
System.err.println("Mask cannot be more than 32, that doesn't make any sense, you can't get any more specific than only the specified IP");
System.exit(1);
}
maskVal = ByteBuffer.allocate(4).putInt(0xFFFFFFFF << (32 - amount)).array();
}catch(NumberFormatException e) {
e.printStackTrace();
System.err.println("Unable to parse Mask address \"" + ip + "\", could not parse " + mask.substring(1) + " as an integer (whole number)");
System.exit(2);
}
}else {
try {
maskVal = Inet4Address.getByName(mask).getAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
System.err.println("Unable to parse Mask address \"" + ip + "\"");
System.exit(2);
}
}
ranges.add(new Range(ipVal, maskVal));
}
}

62
src/edu/clarkson/cosi/wishlist/core/Main.java

@ -0,0 +1,62 @@
package edu.clarkson.cosi.wishlist.core;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import edu.clarkson.cosi.wishlist.WebSys.IWebSys;
import edu.clarkson.cosi.wishlist.WebSys.WebSys;
import edu.clarkson.cosi.wishlist.database.Database;
import edu.clarkson.cosi.wishlist.database.IDatabase;
import net.pop1040.JSON.JSONObject;
public class Main {
static IWebSys webSys;
static IDatabase database;
static AddressMatcher addressMatcher = null;
public static void main(String[] args) {
JSONObject config = Util.loadConfig();
if(!config.isJSONObject("database")) {
System.err.println("\"database\" JSONObject section not found in config file, please fix it or delete it for another to be autogenerated");
System.exit(1);
}
if(!config.isJSONObject("webSystem")) {
System.err.println("\"webSystem\" JSONObject section not found in config file, please fix it or delete it for another to be autogenerated");
System.exit(1);
}
if(!config.isJSONArray("allowedIPRanges")) {
System.err.println("\"allowedIPRanges\" JSONArray section not found in config file, defaulting to allowing all IPs");
}else {
addressMatcher = Util.loadIPRanges(config.getArray("allowedIPRanges"));
}
database = new Database(config.getObject("database"));
webSys = new WebSys(config.getObject("webSystem"));
database.start();
webSys.start();
System.out.println("Started");
}
public static Action actionLookup(String actionName) {
if(database.isAction(actionName))return new Action(actionName);
return null;
}
public static IDatabase getDatabase() {
return database;
}
public static JSONObject processAction(Action action) {
InetAddress address = action.remoteAddress.getAddress();
if(address instanceof Inet6Address)return database.processAction(action); // Override security bla becuase IPv6
if(addressMatcher != null)if(!(address instanceof Inet4Address) || addressMatcher.inRange((Inet4Address) address))return new JSONObject().setString("error", "Unauthorized, your IP address (" + address + ") is not whitelisted").setString("errorType", "Authentication error");
return database.processAction(action);
}
}

99
src/edu/clarkson/cosi/wishlist/core/Util.java

@ -0,0 +1,99 @@
package edu.clarkson.cosi.wishlist.core;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Writer;
import net.pop1040.JSON.JSON;
import net.pop1040.JSON.JSONArray;
import net.pop1040.JSON.JSONObject;
import net.pop1040.JSON.JSONValue;
public class Util {
protected static JSONObject loadConfig() {
InputStream is = null;
String file = "";
try {
//Read config file
is = new FileInputStream("configs.json");
StringBuilder sb = new StringBuilder();
new BufferedReader(new InputStreamReader(is));
int c;
while ((c = is.read()) != -1) {
sb.append((char) c);
}
is.close();
file = sb.toString();
} catch (FileNotFoundException e) {
//Create default config
file = "{\r\n" +
"\t\"database\":{\r\n" +
"\t\t\"databaseDirectory\":\"database/\"\r\n" +
"\t},\r\n" +
"\t\"webSystem\":{\r\n" +
"\t\t\"port\": 8027\r\n" +
"\t},\r\n" +
"\t\"allowedIPRanges\":[\r\n" +
"\t\t{\r\n" +
"\t\t\t\"IP\":\"localhost\",\r\n" +
"\t\t\t\"Mask\":\"/22\"\r\n" +
"\t\t}\r\n" +
"\t]\r\n" +
"}";
try {
//Write default config to new configs file
Writer fw = new FileWriter("configs.json");
fw.write(file);
fw.flush();
fw.close();
} catch (FileNotFoundException e1) {
e1.printStackTrace();
} catch (IOException e1) {
e1.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
JSONValue json = JSON.parse(file);
if (!(json instanceof JSONObject)) {
System.err.println("Unable to parse configs, please delete it for another to be autogenerated");
System.exit(1);
}
return (JSONObject) json;
}
public static AddressMatcher loadIPRanges(JSONArray array) {
AddressMatcher matcher = new AddressMatcher();
for(int i=0; i<array.size(); i++) {
if(array.isJSONObject(i)) {
JSONObject obj = array.getJSONObject(i);
if(obj.isString("IP")) {
if(obj.isString("Mask")) {
String ip = obj.getString("IP");
String mask = obj.getString("Mask");
matcher.addRange(ip, mask);
}else System.err.println("Element " + i + " does not contain the String \"Mask\", which can contain either a forward slash followed by how many bits must match the IP, or a subnet mask style IP address (like 255.255.255.0)");
}else System.err.println("Element " + i + " does not contain the String \"IP\", which can contain either an IPv4 address or the text localhost");
}else System.err.println("Element " + i + " in allowedIPRanges array is not a JSON Object, ignoring");
}
return matcher;
}
}

139
src/edu/clarkson/cosi/wishlist/database/Database.java

@ -0,0 +1,139 @@
package edu.clarkson.cosi.wishlist.database;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.QueryExecutionException;
import org.neo4j.graphdb.Result;
import org.neo4j.graphdb.factory.GraphDatabaseFactory;
import edu.clarkson.cosi.wishlist.core.Action;
import net.pop1040.JSON.JSONArray;
import net.pop1040.JSON.JSONObject;
public class Database implements IDatabase{
protected String databaseDirectory;
HashMap<String, IDBAction> actionHandlers = new HashMap<>();
GraphDatabaseService db;
public Database(JSONObject config) {
if(config.isString("databaseDirectory")) {
databaseDirectory = config.getString("databaseDirectory");
}else {
databaseDirectory = "database/";
System.out.println("The string \"databaseDirectory\" was not specified in the database portion of the config file, defaulting to \"database/\"");
}
registerActionHandlers();
}
@Override
public void start() {
//we use the embedded database builder so we can set the folder for the database before initialization.
System.out.println("Starting internal database");
final GraphDatabaseService db = new GraphDatabaseFactory().newEmbeddedDatabaseBuilder(new File(databaseDirectory)).newGraphDatabase();
Runtime.getRuntime().addShutdownHook(new Thread(()->{
db.shutdown();
System.out.println("database shut down");
})); //make sure database is shut down on close
this.db = db;
System.out.println("Database started");
}
@Override
public JSONObject processAction(Action action) {
IDBAction actionHandle = actionHandlers.get(action.getActionName());
if(actionHandle == null)return new JSONObject().setString("error", "Action unknown to database").setString("errorType", "Internal error");
return actionHandle.process(action, db);
}
@Override
public boolean isAction(String actionName) {
return actionHandlers.containsKey(actionName);
}
private void registerActionHandlers() {
actionHandlers.put("searchComments", (Action action, GraphDatabaseService database)->{
String name = ""; //name search
String query = ""; //search query, the text to search for
if(action.getJson().isString("name"))name = action.getJson().getString("name");
if(action.getJson().isString("text"))query = action.getJson().getString("text");
JSONArray arry = new JSONArray();
ArrayList<Map<String, Object>> res = queryAll("Match (n:Comment)-[:By]->(p:User) Where toLower(n.text) CONTAINS $text and toLower(p.name) CONTAINS $name Return n.text as text, p.name as name", new Object[][] {{"name", name.toLowerCase()}, {"text", query.toLowerCase()}}, db);
for(Map<String, Object> iter : res) {
Object tex = iter.get("text");
Object nm = iter.get("name");
if(tex instanceof String && nm instanceof String) {
arry.addJSONObject(new JSONObject().setString("text", (String) tex).setString("name", (String) nm));
}
}
return new JSONObject().setBoolean("success", true).setJSONArray("comments", arry);
});
actionHandlers.put("addComment", (Action action, GraphDatabaseService database)->{
if(!action.getJson().isString("name") || !action.getJson().isString("text"))return new JSONObject().setString("error", "both name and text must be specified for submitting a comment").setString("errorType", "Formatting error");
String name = action.getJson().getString("name"); //name
String text = action.getJson().getString("text"); //comment body
query("Merge (p:User{name: $name}) With p Create (n:Comment{text: $text})-[:By]->(p)", new Object[][] {{"name", name}, {"text", text}}, db);
return new JSONObject().setBoolean("success", true);
});
}
/**
* Makes a Cypher query and returns only one row
* @param query
* @param db
* @return
*/
protected static Map<String, Object> query(String query, Object[][] params, GraphDatabaseService db) {
try (Result result = db.execute(query, Stream.of(params).collect(Collectors.toMap(data -> (String) data[0], data -> data[1])))) {
if(result.hasNext()) {
return result.next();
}
}
return null;
}
protected static Map<String, Object> querySafe(String query, Object[][] params, GraphDatabaseService db){
Map<String, Object> res = query(query, params, db);
return res==null?new HashMap<>():res;
}
/**
* Makes a Cypher query and returns all rows
* @param query
* @param db
* @return
*/
protected static ArrayList<Map<String, Object>> queryAll(String query, Object[][] params, GraphDatabaseService db) {
try (Result result = db.execute(query, Stream.of(params).collect(Collectors.toMap(data -> (String) data[0], data -> data[1])))) {
ArrayList<Map<String, Object>> output = new ArrayList<>();
while(result.hasNext())output.add(result.next());
return output;
}catch(QueryExecutionException e) {
return null;
}
}
@SuppressWarnings("unchecked") //because it actually is, but dynamically because generic methods don't support the normal instance of method
protected static <T> ArrayList<T> queryAll(String query, Object[][] params, GraphDatabaseService db, String collum, Class<T> clazz) {
ArrayList<Map<String, Object>> q = queryAll(query, params, db);
ArrayList<T> output = new ArrayList<>();
for(Map<String, Object> iter : q)if(iter.get(collum).getClass() == clazz)output.add((T) iter.get(collum));
return output;
}
}

12
src/edu/clarkson/cosi/wishlist/database/IDBAction.java

@ -0,0 +1,12 @@
package edu.clarkson.cosi.wishlist.database;
import org.neo4j.graphdb.GraphDatabaseService;
import edu.clarkson.cosi.wishlist.core.Action;
import net.pop1040.JSON.JSONObject;
public interface IDBAction {
JSONObject process(Action action, GraphDatabaseService database);
}

12
src/edu/clarkson/cosi/wishlist/database/IDatabase.java

@ -0,0 +1,12 @@
package edu.clarkson.cosi.wishlist.database;
import edu.clarkson.cosi.wishlist.core.Action;
import net.pop1040.JSON.JSONObject;
public interface IDatabase {
public void start();
JSONObject processAction(Action action);
public boolean isAction(String actionName);
}

18
src/net/pop1040/JSON/JSON.java

@ -0,0 +1,18 @@
package net.pop1040.JSON;
import java.io.IOException;
import java.util.PrimitiveIterator.OfInt;
public class JSON {
public static JSONValue parse(String s) {
OfInt iter = s.codePoints().iterator();
try {
return JSONValue.parseValue(iter).val;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}

175
src/net/pop1040/JSON/JSONArray.java

@ -0,0 +1,175 @@
package net.pop1040.JSON;
import java.io.IOException;
import java.util.ArrayList;
import java.util.PrimitiveIterator.OfInt;
import net.pop1040.JSON.JSONValue.JSONNum.JSONFloat;
import net.pop1040.JSON.JSONValue.JSONNum.JSONInt;
public class JSONArray extends JSONValue {
ArrayList<JSONValue> values;
public JSONArray(ArrayList<JSONValue> val) {
this.values = val;
}
public JSONArray() {
values = new ArrayList<>();
}
public static JSONArray parse(OfInt s) throws IOException {
ArrayList<JSONValue> values = new ArrayList<JSONValue>();
JSONValue.ParseRet r; //parse val and add to array, if the next char is a ] then the array has ended.
while((r = JSONValue.parseValue(s)).lastChar != ']')if(r.lastChar == ',')values.add(r.val);else throw new IOException("Invalid JSON array");
if(!r.isEnd)values.add(r.val); //If the array is empty, ParseRet will set the isEnd flag, so we know that the value is not null
return new JSONArray(values);
}
public JSONValue getValue(int i) {
return values.get(i);
}
public String getString(int i) {
return ((JSONString)values.get(i)).val;
}
public long getLong(int i) {
return ((JSONInt)values.get(i)).val;
}
public double getDouble(int i) {
return ((JSONFloat)values.get(i)).val;
}
public boolean getBoolean(int i) {
return ((JSONBool)values.get(i)).val;
}
public JSONArray getJSONArray(int i) {
return ((JSONArray)values.get(i));
}
public JSONObject getJSONObject(int i) {
return ((JSONObject)values.get(i));
}
/**
* Get the size of this array
* @return
*/
public int size() {
return values.size();
}
private void ensureCapacity(int i) {
while(this.size()<i)values.add(new JSONNull());
}
public JSONArray setValue(JSONValue v, int i) {
ensureCapacity(i+1);
values.set(i, v);
return this;
}
public JSONArray setLong(long l, int i) {
setValue(new JSONInt(l), i);
return this;
}
public JSONArray setDouble(double d, int i) {
setValue(new JSONFloat(d), i);
return this;
}
public JSONArray setString(String s, int i) {
setValue(new JSONString(s), i);
return this;
}
public JSONArray setNull(int i) {
setValue(new JSONNull(), i);
return this;
}
public JSONArray setBoolean(boolean b, int i) {
setValue(new JSONBool(b), i);
return this;
}
public JSONArray setJSONObject(JSONObject obj, int i) {
setValue(obj, i);
return this;
}
public JSONArray setJSONArray(JSONArray arr, int i) {
setValue(arr, i);
return this;
}
public JSONArray addValue(JSONValue v) {
values.add(v);
return this;
}
public JSONArray addLong(long l) {
addValue(new JSONInt(l));
return this;
}
public JSONArray addDouble(double d) {
addValue(new JSONFloat(d));
return this;
}
public JSONArray addString(String s) {
addValue(new JSONString(s));
return this;
}
public JSONArray addNull() {
addValue(new JSONNull());
return this;
}
public JSONArray addBoolean(boolean b) {
addValue(new JSONBool(b));
return this;
}
public JSONArray addJSONObject(JSONObject obj) {
addValue(obj);
return this;
}
public JSONArray addJSONArray(JSONArray arr) {
addValue(arr);
return this;
}
public boolean exists(int i) {
return i>=0 && i<values.size();
}
public boolean isString(int i) {
if(exists(i))return getValue(i) instanceof JSONString;
return false;
}
public boolean isNum(int i) {
if(exists(i))return getValue(i) instanceof JSONNum;
return false;
}
public boolean isLong(int i) {
if(exists(i))return getValue(i) instanceof JSONInt;
return false;
}
public boolean isDouble(int i) {
if(exists(i))return getValue(i) instanceof JSONFloat;
return false;
}
public boolean isJSONObject(int i) {
if(exists(i))return getValue(i) instanceof JSONObject;
return false;
}
public boolean isJSONArray(int i) {
if(exists(i))return getValue(i) instanceof JSONArray;
return false;
}
public boolean isNull(int i) {
if(exists(i))return getValue(i) instanceof JSONNull || getValue(i) == null;
return false;
}
public boolean isBoolean(int i) {
if(exists(i))return getValue(i) instanceof JSONBool;
return false;
}
@Override
public String toString() {
StringBuilder buff = new StringBuilder();
buff.append('[');
for(int i=0; i<values.size(); i++) {
if(i!=0)buff.append(',');
buff.append(values.get(i).toString());
}
buff.append(']');
return buff.toString();
}
}

153
src/net/pop1040/JSON/JSONObject.java

@ -0,0 +1,153 @@
package net.pop1040.JSON;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.PrimitiveIterator.OfInt;
import net.pop1040.JSON.JSONValue.JSONNum.JSONFloat;
import net.pop1040.JSON.JSONValue.JSONNum.JSONInt;
public class JSONObject extends JSONValue{
HashMap<String, JSONValue> values;
protected JSONObject(HashMap<String, JSONValue> vals) {
this.values = vals;
}
public JSONObject() {
values = new HashMap<>();
}
public static JSONObject parse(OfInt s) throws IOException {
HashMap<String, JSONValue> vals = new HashMap<String, JSONValue>();
boolean firstRun = true;
while(true) {
int c = JSONValue.nextNonWhitespace(s);
if(c == '}') {
if(firstRun)break;
else throw new IOException("Invalid JSON Object, } was directly after ,");
}
if(c != '"')throw new IOException("Invalid JSON Object, key is not valid string");
String name = JSONValue.parseString(s);
c = JSONValue.nextNonWhitespace(s);
if(c != ':')throw new IOException("Invalid JSON Object, : was not found after key");
JSONValue.ParseRet val = JSONValue.parseValue(s);
if(val.isEnd)throw new IOException("Invalid JSON Object, encountered " + val.lastChar + " is not a JSON value");
vals.put(name, val.val);
if(val.lastChar == '}')break;
if(val.lastChar != ',')throw new IOException("Invalid JSON Object, encountered " + val.lastChar + " after a value, expected either , or }");
firstRun = false;
}
return new JSONObject(vals);
}
public JSONValue getValue(String key) {
return values.get(key);
}
public String getString(String key) {
return ((JSONString)values.get(key)).val;
}
public long getLong(String key) {
return ((JSONInt)values.get(key)).val;
}
public double getDouble(String key) {
return ((JSONFloat)values.get(key)).val;
}
public boolean getBoolean(String key) {
return ((JSONBool)values.get(key)).val;
}
public JSONArray getArray(String key) {
return ((JSONArray)values.get(key));
}
public JSONObject getObject(String key) {
return ((JSONObject)values.get(key));
}
public boolean exists(String key) {
return values.containsKey(key);
}
public boolean isString(String key) {
return values.get(key) instanceof JSONString;
}
public boolean isLong(String key) {
return values.get(key) instanceof JSONInt;
}
public boolean isDouble(String key) {
return values.get(key) instanceof JSONFloat;
}
public boolean isBoolean(String key) {
return values.get(key) instanceof JSONBool;
}
public boolean isNull(String key) {
return values.get(key) instanceof JSONNull || (values.containsKey(key) && values.get(key) == null);
}
public boolean isJSONObject(String key) {
return values.get(key) instanceof JSONObject;
}
public boolean isJSONArray(String key) {
return values.get(key) instanceof JSONArray;
}
public JSONObject setNull(String key) {
values.put(key, new JSONNull());
return this;
}
public JSONObject setString(String key, String val) {
values.put(key, new JSONString(val));
return this;
}
public JSONObject setLong(String key, long val) {
values.put(key, new JSONInt(val));
return this;
}
public JSONObject setDouble(String key, double val) {
values.put(key, new JSONFloat(val));
return this;
}
public JSONObject setBoolean(String key, boolean val) {
values.put(key, new JSONBool(val));
return this;
}
public JSONObject setJSONObject(String key, JSONObject val) {
values.put(key, val);
return this;
}
public JSONObject setJSONArray(String key, JSONArray val) {
values.put(key, val);
return this;
}
@Override
public String toString() {
StringBuilder buff = new StringBuilder();
buff.append('{');
Iterator<Entry<String, JSONValue>> iter = values.entrySet().iterator();
if(iter.hasNext()) {
Entry<String, JSONValue> first = iter.next();
buff.append(new JSONString(first.getKey()).toString());
buff.append(':');
buff.append(first.getValue().toString());
}
while(iter.hasNext()){
Entry<String, JSONValue> next = iter.next();
buff.append(',');
buff.append(new JSONString(next.getKey()).toString());
buff.append(':');
buff.append(next.getValue().toString());
}
buff.append('}');
return buff.toString();
}
public String[] getKeys() {
return values.keySet().toArray(new String[] {});
}
}

271
src/net/pop1040/JSON/JSONValue.java

@ -0,0 +1,271 @@
package net.pop1040.JSON;
import java.io.IOException;
import java.util.PrimitiveIterator.OfInt;
public class JSONValue {
public static class JSONString extends JSONValue{
String val;
public JSONString(String val) {
this.val = val;
}
@Override
public String toString() {
StringBuilder buff = new StringBuilder();
buff.append('\"');
for(int i=0; i<val.length(); i++) {
char c = val.charAt(i);
switch(c) {
case '\"': buff.append("\\\""); break;
case '\\': buff.append("\\\\"); break;
case '/': buff.append("\\/" ); break;
case '\b': buff.append("\\b"); break;
case '\r': buff.append("\\r"); break;
case '\n': buff.append("\\n"); break;
case '\f': buff.append("\\f"); break;
case '\t': buff.append("\\t"); break;
default: buff.append(c); break;
}
}
buff.append('\"');
return buff.toString();
}
}
public static class JSONNum extends JSONValue{
protected static class JSONInt extends JSONNum{
long val;
public JSONInt(long l) {
this.val = l;
}
@Override
public String toString() {
return Long.toString(val);
}
}
protected static class JSONFloat extends JSONNum{
double val;
public JSONFloat(double val) {
this.val = val;
}
@Override
public String toString() {
return Double.toString(val);
}
}
}
public static class JSONBool extends JSONValue{
boolean val;
public JSONBool(boolean val) {
this.val = val;
}
@Override
public String toString() {
return val ? "true" : "false";
}
}
public static class JSONNull extends JSONValue{
@Override
public String toString() {
return "null";
}
}
protected static String parseString(OfInt s) {
StringBuilder buf = new StringBuilder(); //place to store created string
while(s.hasNext()) { //so we exit if we run out of input, we should return before that
int c = s.nextInt();
switch(c) {
case '"': //we've hit the end of the string, return
return buf.toString();
case '\\': //need to deal with escaped characters
if(!s.hasNext())return null;
switch(s.nextInt()) {
case '"' :buf.append('"' );break; //most escaped characters are simple
case '\\':buf.append('\\');break;
case '/' :buf.append('/' );break;
case 'b' :buf.append('\b');break;
case 'f' :buf.append('\f');break;
case 'n' :buf.append('\n');break;
case 'r' :buf.append('\r');break;
case 't' :buf.append('\t');break;
case 'u' : // \\u is annoying though, its followed by four hex chars
char[] hexChars = new char[4]; //buffer for four (it is always four) hex chars
for(int i=0; i<4; i++)if(s.hasNext()){ //read them in
int hexChar = s.nextInt();
if(!((hexChar >= '0' && hexChar <= '9') ||
(hexChar >= 'a' && hexChar <= 'f') ||
(hexChar >= 'A' && hexChar <= 'F')))return null; //make sure they are in fact valid hex
hexChars[i] = (char)hexChar; //put in buffer
}else return null; //unexpected end of input, we bail
buf.append((char) Integer.parseInt(new String(hexChars), 16)); //parse buffer of hex chars to integer, cast to character and append
//If like me you wonder how full unicode is encoded with \\u if \\u only supports four hex characters which is only 16 bit, anything
//past the Basic Multilingual Plane as its called (anything beyond 16bit) is stored in surrogate pairs just like how java handles things,
//so since string builder and strings just use characters, and thus also use surrogate pairs, adding a character like this means that if
//the JSON file contained valid surrogate pairs, so will the string, everything is handled for us
break;
default: //this is an invalid escape, bad string.
return null;
}
break;
default: //add normal characters, reason for the code point stuff is because JSON can be in UTF8, and we don't want emoji breaking our system
buf.appendCodePoint(c); //java uses 16bit characters (be glad we aren't dealing with 8bit characters for this), so emoji are stored in UTF-16 surrogate pairs normally
} //So that we don't have to deal with those manually, I'm just handling everything as codepoints, ints (32bit) that store a full unicode character.
}
return null;
}
protected static ParseRet parseValue(OfInt s) throws IOException {
int c = nextNonWhitespace(s);
switch(c) {
case '{': return new ParseRet(JSONObject.parse(s), nextNonWhitespace(s));
case '[': return new ParseRet(JSONArray.parse(s), nextNonWhitespace(s));
case '}': return new ParseRet((int)'}'); //Not actually a value, but rather the object ends early
case ']': return new ParseRet((int)']');
case 'n':{
if(!s.hasNext()) throw new IOException("JSON null was not completed");
if(s.nextInt() != 'u')throw new IOException("JSON null was not completed");
if(!s.hasNext()) throw new IOException("JSON null was not completed");
if(s.nextInt() != 'l')throw new IOException("JSON null was not completed");
if(!s.hasNext()) throw new IOException("JSON null was not completed");
if(s.nextInt() != 'l')throw new IOException("JSON null was not completed");
return new ParseRet(new JSONNull(), nextNonWhitespace(s));
}
case 'f':{
if(!s.hasNext()) throw new IOException("JSON boolean false was not completed");
if(s.nextInt() != 'a')throw new IOException("JSON boolean false was not completed");
if(!s.hasNext()) throw new IOException("JSON boolean false was not completed");
if(s.nextInt() != 'l')throw new IOException("JSON boolean false was not completed");
if(!s.hasNext()) throw new IOException("JSON boolean false was not completed");
if(s.nextInt() != 's')throw new IOException("JSON boolean false was not completed");
if(!s.hasNext()) throw new IOException("JSON boolean false was not completed");
if(s.nextInt() != 'e')throw new IOException("JSON boolean false was not completed");
return new ParseRet(new JSONBool(false), nextNonWhitespace(s));
}
case 't':{
if(!s.hasNext()) throw new IOException("JSON boolean true was not completed");
if(s.nextInt() != 'r')throw new IOException("JSON boolean true was not completed");
if(!s.hasNext()) throw new IOException("JSON boolean true was not completed");
if(s.nextInt() != 'u')throw new IOException("JSON boolean true was not completed");
if(!s.hasNext()) throw new IOException("JSON boolean true was not completed");
if(s.nextInt() != 'e')throw new IOException("JSON boolean true was not completed");
return new ParseRet(new JSONBool(true), nextNonWhitespace(s));
}
case '\"': return new ParseRet(new JSONString(parseString(s)), nextNonWhitespace(s));
default: //We'll now try to parse a number
boolean negative = false;
boolean powerNegative = false;
if(c == '-') { //first check if its negative, if so we set a flag and get another character for the next section
negative = true;
if(!s.hasNext())throw new IOException("parsing number, ended unexpectedly after reading just a -");
c = s.nextInt();
}
byte state = 0;
StringBuilder power = null, decimal = null, digit; //buffer for the digits before the decimal point, after but before an E, and after the E
digit = new StringBuilder(); //there should always be at least one digit before the decimal
while(true) {
if(c >= '0' && c <= '9')switch(state) {
case 0: digit .appendCodePoint(c);break;
case 1: decimal.appendCodePoint(c);break;
case 2: power .appendCodePoint(c);break;
}else switch(state) {
case 0:
if(c == '.') {
if(digit.length() == 0)throw new IOException("JSON number must contain atleast one digit before a decmial");
state = 1;
decimal = new StringBuilder();
}else if(c == 'e' || c == 'E') {
if(digit.length() == 0)throw new IOException("JSON number must contain atleast one digit before scientific power");
state = 2;
power = new StringBuilder();
}else {
if(digit.length() == 0) {
if(negative)throw new IOException("JSON number must contain atleast one digit, a singular hyphen is not a number");
else throw new IOException("Malformed JSON, not a valid value where a value was expected");
}
return new ParseRet(new JSONNum.JSONInt(negative?-Long.parseLong(digit.toString()):Long.parseLong(digit.toString())), nextNonWhitespace(c, s));
}
break;
case 1:
if(c == 'e' || c == 'E') {
if(decimal.length() == 0)throw new IOException("JSON number must contain atleast one digit between a decimal and scientific power");
state = 2;
power = new StringBuilder();
}else {
if(decimal.length() == 0)throw new IOException("JSON number has to have atleast one digit after a decimal");
return new ParseRet(new JSONNum.JSONFloat(negative?-Double.parseDouble(digit.toString() + '.' + decimal.toString()):Double.parseDouble(digit.toString() + '.' + decimal.toString())), nextNonWhitespace(c, s));
}
break;
case 2:
if(c == '-') {
if(power.length() != 0)throw new IOException("unexpectedly encountered - in the middle of parsing scientific notation");
powerNegative = true;
if(!s.hasNext())throw new IOException("parsing number, ended unexpectedly after reading a negative sign after a scientific notation marker (e or E)");
c = s.nextInt();
}else if(c == '+') {
if(power.length() != 0)throw new IOException("unexpectedly encountered + in the middle of parsing scientific notation");
if(!s.hasNext())throw new IOException("parsing number, ended unexpectedly after reading a positive sign after a scientific notation marker (e or E)");
c = s.nextInt();
}else {
if(power.length() == 0)throw new IOException("JSON number has to have atleast one digit after an e or E denoting a scientific power");
if(decimal == null || decimal.length() == 0) {
return new ParseRet(new JSONNum.JSONFloat(negative?-Double.parseDouble(digit.toString() + (powerNegative?"E-":"E") + power.toString()):Double.parseDouble(digit.toString() + (powerNegative?"E-":"E") + power.toString())), nextNonWhitespace(c, s));
}
return new ParseRet(new JSONNum.JSONFloat(negative?-Double.parseDouble(digit.toString() + '.' + decimal.toString() + (powerNegative?"E-":"E") + power.toString()):Double.parseDouble(digit.toString() + '.' + decimal.toString() + (powerNegative?"E-":"E") + power.toString())), nextNonWhitespace(c, s));
}
break;
}
if(!s.hasNext())throw new IOException("parsing number, ended unexpectedly during parsing a number");
c = s.nextInt();
}
}
// return new ParseRet(null, 0);
}
public static class ParseRet{
boolean isEnd;
public JSONValue val;
public int lastChar;
public ParseRet(JSONValue val, int lastChar) {
this.val = val;
this.lastChar = lastChar;
this.isEnd = false;
}
public ParseRet(int lastChar) {
this.isEnd = true;
this.lastChar = lastChar;
}
@Override
public String toString() {
return "<" + isEnd + "," + val + "," + new String(Character.toChars(lastChar)) + "(" + lastChar + ")>";
}
}
/**
*
* Gets the next character (codepoint specifically, because unicode support) that isn't whitespace.
* If the end of the input is encountered, 0 is returned
* @param s input
* @return next non-whitespace codepoint, 0 if none left
*/
protected static int nextNonWhitespace(OfInt s){
while(true) {
if(!s.hasNext())return 0;
int n = s.nextInt();
if(n != '\t' && n != '\r' && n != '\n' && n != ' ')return n;
}
}
protected static int nextNonWhitespace(int n, OfInt s){
if(n != '\t' && n != '\r' && n != '\n' && n != ' ')return n;
return nextNonWhitespace(s);
}
}
Loading…
Cancel
Save