From 556474db93a4cd5041edcf8f13b838b0c13ff571 Mon Sep 17 00:00:00 2001 From: Zak Yani Star Fenton Date: Wed, 11 Jun 2025 01:08:13 +1000 Subject: [PATCH] Added project files --- build.xml | 73 + manifest.mf | 3 + nbproject/build-impl.xml | 1771 ++++++++++++++ nbproject/genfiles.properties | 8 + nbproject/project.properties | 95 + nbproject/project.xml | 15 + src/swap/auth/AuthService.java | 25 + src/swap/auth/BasicCrypto.java | 67 + src/swap/auth/Session.java | 20 + src/swap/data/DataAbstraction.java | 71 + src/swap/data/DataConv.java | 115 + src/swap/data/DataFile.java | 317 +++ src/swap/data/DataObject.java | 411 ++++ src/swap/data/DataOp.java | 5 + src/swap/data/DataSet.java | 52 + src/swap/data/DataString.java | 139 ++ src/swap/data/DataTransaction.java | 34 + src/swap/data/DataVariable.java | 21 + src/swap/dbtool/Main.java | 13 + src/swap/httpd/Content.java | 12 + src/swap/httpd/Handler.java | 5 + src/swap/httpd/Header.java | 19 + src/swap/httpd/HeaderSet.java | 41 + src/swap/httpd/ListenerThread.java | 44 + src/swap/httpd/LoggedServerGroup.java | 60 + src/swap/httpd/Request.java | 37 + src/swap/httpd/Response.java | 81 + src/swap/httpd/SecureListenerThread.java | 64 + src/swap/httpd/ServerGroup.java | 150 ++ src/swap/library/LibraryLogin.java | 54 + src/swap/library/LibrarySection.java | 18 + src/swap/library/LibrarySet.java | 98 + src/swap/library/LibraryShelf.java | 23 + src/swap/log/Log.java | 74 + src/swap/log/PrintStreamLog.java | 27 + src/swap/old/othercereal/Field.java | 9 + src/swap/old/othercereal/Image.java | 9 + src/swap/old/othercereal/Method.java | 11 + src/swap/old/othercereal/ObjectSet.java | 9 + src/swap/old/othercereal/Type.java | 9 + src/swap/old/othercereal/TypeSet.java | 13 + src/swap/old/pojocereal/Boxer.java | 107 + src/swap/old/pojocereal/Brand.java | 30 + src/swap/old/pojocereal/CerealException.java | 29 + src/swap/old/pojocereal/Field.java | 57 + src/swap/old/pojocereal/Grain.java | 5 + src/swap/old/pojocereal/Ingredient.java | 9 + src/swap/old/pojocereal/Type.java | 13 + src/swap/superdata/SuperBin.java | 43 + src/swap/superdata/SuperDoc.java | 68 + src/swap/superdata/SuperLock.java | 46 + src/swap/superdata/SuperSet.java | 312 +++ src/swap/superdata/SuperVersion.java | 20 + src/swap/template/Attribute.java | 19 + src/swap/template/Element.java | 116 + src/swap/template/Node.java | 39 + src/swap/template/Page.java | 63 + src/swap/template/StructuredPage.java | 10 + src/swap/template/Text.java | 78 + src/swap/testing/DBTest.java | 74 + src/swap/testing/SuperTest.java | 39 + src/swap/util/JSON.java | 2279 ++++++++++++++++++ src/swap/webwall/Main.java | 60 + 63 files changed, 7638 insertions(+) create mode 100644 build.xml create mode 100644 manifest.mf create mode 100644 nbproject/build-impl.xml create mode 100644 nbproject/genfiles.properties create mode 100644 nbproject/project.properties create mode 100644 nbproject/project.xml create mode 100644 src/swap/auth/AuthService.java create mode 100644 src/swap/auth/BasicCrypto.java create mode 100644 src/swap/auth/Session.java create mode 100644 src/swap/data/DataAbstraction.java create mode 100644 src/swap/data/DataConv.java create mode 100644 src/swap/data/DataFile.java create mode 100644 src/swap/data/DataObject.java create mode 100644 src/swap/data/DataOp.java create mode 100644 src/swap/data/DataSet.java create mode 100644 src/swap/data/DataString.java create mode 100644 src/swap/data/DataTransaction.java create mode 100644 src/swap/data/DataVariable.java create mode 100644 src/swap/dbtool/Main.java create mode 100644 src/swap/httpd/Content.java create mode 100644 src/swap/httpd/Handler.java create mode 100644 src/swap/httpd/Header.java create mode 100644 src/swap/httpd/HeaderSet.java create mode 100644 src/swap/httpd/ListenerThread.java create mode 100644 src/swap/httpd/LoggedServerGroup.java create mode 100644 src/swap/httpd/Request.java create mode 100644 src/swap/httpd/Response.java create mode 100644 src/swap/httpd/SecureListenerThread.java create mode 100644 src/swap/httpd/ServerGroup.java create mode 100644 src/swap/library/LibraryLogin.java create mode 100644 src/swap/library/LibrarySection.java create mode 100644 src/swap/library/LibrarySet.java create mode 100644 src/swap/library/LibraryShelf.java create mode 100644 src/swap/log/Log.java create mode 100644 src/swap/log/PrintStreamLog.java create mode 100644 src/swap/old/othercereal/Field.java create mode 100644 src/swap/old/othercereal/Image.java create mode 100644 src/swap/old/othercereal/Method.java create mode 100644 src/swap/old/othercereal/ObjectSet.java create mode 100644 src/swap/old/othercereal/Type.java create mode 100644 src/swap/old/othercereal/TypeSet.java create mode 100644 src/swap/old/pojocereal/Boxer.java create mode 100644 src/swap/old/pojocereal/Brand.java create mode 100644 src/swap/old/pojocereal/CerealException.java create mode 100644 src/swap/old/pojocereal/Field.java create mode 100644 src/swap/old/pojocereal/Grain.java create mode 100644 src/swap/old/pojocereal/Ingredient.java create mode 100644 src/swap/old/pojocereal/Type.java create mode 100644 src/swap/superdata/SuperBin.java create mode 100644 src/swap/superdata/SuperDoc.java create mode 100644 src/swap/superdata/SuperLock.java create mode 100644 src/swap/superdata/SuperSet.java create mode 100644 src/swap/superdata/SuperVersion.java create mode 100644 src/swap/template/Attribute.java create mode 100644 src/swap/template/Element.java create mode 100644 src/swap/template/Node.java create mode 100644 src/swap/template/Page.java create mode 100644 src/swap/template/StructuredPage.java create mode 100644 src/swap/template/Text.java create mode 100644 src/swap/testing/DBTest.java create mode 100644 src/swap/testing/SuperTest.java create mode 100644 src/swap/util/JSON.java create mode 100644 src/swap/webwall/Main.java diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..53b02df --- /dev/null +++ b/build.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + Builds, tests, and runs the project slswap. + + + diff --git a/manifest.mf b/manifest.mf new file mode 100644 index 0000000..328e8e5 --- /dev/null +++ b/manifest.mf @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +X-COMMENT: Main-Class will be added automatically by build + diff --git a/nbproject/build-impl.xml b/nbproject/build-impl.xml new file mode 100644 index 0000000..0eceb08 --- /dev/null +++ b/nbproject/build-impl.xml @@ -0,0 +1,1771 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set src.dir + Must set test.src.dir + Must set build.dir + Must set dist.dir + Must set build.classes.dir + Must set dist.javadoc.dir + Must set build.test.classes.dir + Must set build.test.results.dir + Must set build.classes.excludes + Must set dist.jar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set javac.includes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + No tests executed. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set JVM to use for profiling in profiler.info.jvm + Must set profiler agent JVM arguments in profiler.info.jvmargs.agent + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select some files in the IDE or set javac.includes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + To run this application from the command line without Ant, try: + + java -jar "${dist.jar.resolved}" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set run.class + + + + Must select one file in the IDE or set run.class + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set debug.class + + + + + Must select one file in the IDE or set debug.class + + + + + Must set fix.includes + + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + Must select one file in the IDE or set profile.class + This target only works when run from inside the NetBeans IDE. + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set run.class + + + + + + Must select some files in the IDE or set test.includes + + + + + Must select one file in the IDE or set run.class + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select some files in the IDE or set javac.includes + + + + + + + + + + + + + + + + + + + + + + + + Some tests failed; see details above. + + + + + + + + + Must select some files in the IDE or set test.includes + + + + Some tests failed; see details above. + + + + Must select some files in the IDE or set test.class + Must select some method in the IDE or set test.method + + + + Some tests failed; see details above. + + + + + Must select one file in the IDE or set test.class + + + + Must select one file in the IDE or set test.class + Must select some method in the IDE or set test.method + + + + + + + + + + + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nbproject/genfiles.properties b/nbproject/genfiles.properties new file mode 100644 index 0000000..f5ff25f --- /dev/null +++ b/nbproject/genfiles.properties @@ -0,0 +1,8 @@ +build.xml.data.CRC32=0217b7f7 +build.xml.script.CRC32=5cfed110 +build.xml.stylesheet.CRC32=f85dc8f2@1.112.0.48 +# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml. +# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you. +nbproject/build-impl.xml.data.CRC32=0217b7f7 +nbproject/build-impl.xml.script.CRC32=cb2275c5 +nbproject/build-impl.xml.stylesheet.CRC32=12e0a6c2@1.112.0.48 diff --git a/nbproject/project.properties b/nbproject/project.properties new file mode 100644 index 0000000..c21f425 --- /dev/null +++ b/nbproject/project.properties @@ -0,0 +1,95 @@ +annotation.processing.enabled=true +annotation.processing.enabled.in.editor=false +annotation.processing.processor.options= +annotation.processing.processors.list= +annotation.processing.run.all.processors=true +annotation.processing.source.output=${build.generated.sources.dir}/ap-source-output +build.classes.dir=${build.dir}/classes +build.classes.excludes=**/*.java,**/*.form +# This directory is removed when the project is cleaned: +build.dir=build +build.generated.dir=${build.dir}/generated +build.generated.sources.dir=${build.dir}/generated-sources +# Only compile against the classpath explicitly listed here: +build.sysclasspath=ignore +build.test.classes.dir=${build.dir}/test/classes +build.test.results.dir=${build.dir}/test/results +# Uncomment to specify the preferred debugger connection transport: +#debug.transport=dt_socket +debug.classpath=\ + ${run.classpath} +debug.modulepath=\ + ${run.modulepath} +debug.test.classpath=\ + ${run.test.classpath} +debug.test.modulepath=\ + ${run.test.modulepath} +# Files in build.classes.dir which should be excluded from distribution jar +dist.archive.excludes= +# This directory is removed when the project is cleaned: +dist.dir=dist +dist.jar=${dist.dir}/slswap.jar +dist.javadoc.dir=${dist.dir}/javadoc +dist.jlink.dir=${dist.dir}/jlink +dist.jlink.output=${dist.jlink.dir}/slswap +excludes= +includes=** +jar.compress=false +javac.classpath= +# Space-separated list of extra javac options +javac.compilerargs= +javac.deprecation=false +javac.external.vm=true +javac.modulepath= +javac.processormodulepath= +javac.processorpath=\ + ${javac.classpath} +javac.source=21 +javac.target=21 +javac.test.classpath=\ + ${javac.classpath}:\ + ${build.classes.dir} +javac.test.modulepath=\ + ${javac.modulepath} +javac.test.processorpath=\ + ${javac.test.classpath} +javadoc.additionalparam= +javadoc.author=false +javadoc.encoding=${source.encoding} +javadoc.html5=false +javadoc.noindex=false +javadoc.nonavbar=false +javadoc.notree=false +javadoc.private=false +javadoc.splitindex=true +javadoc.use=true +javadoc.version=false +javadoc.windowtitle= +# The jlink additional root modules to resolve +jlink.additionalmodules= +# The jlink additional command line parameters +jlink.additionalparam= +jlink.launcher=true +jlink.launcher.name=slswap +main.class= +manifest.file=manifest.mf +meta.inf.dir=${src.dir}/META-INF +mkdist.disabled=false +platform.active=default_platform +run.classpath=\ + ${javac.classpath}:\ + ${build.classes.dir} +# Space-separated list of JVM arguments used when running the project. +# You may also define separate properties like run-sys-prop.name=value instead of -Dname=value. +# To set system properties for unit tests define test-sys-prop.name=value: +run.jvmargs= +run.modulepath=\ + ${javac.modulepath} +run.test.classpath=\ + ${javac.test.classpath}:\ + ${build.test.classes.dir} +run.test.modulepath=\ + ${javac.test.modulepath} +source.encoding=UTF-8 +src.dir=src +test.src.dir=test diff --git a/nbproject/project.xml b/nbproject/project.xml new file mode 100644 index 0000000..e312c22 --- /dev/null +++ b/nbproject/project.xml @@ -0,0 +1,15 @@ + + + org.netbeans.modules.java.j2seproject + + + slswap + + + + + + + + + diff --git a/src/swap/auth/AuthService.java b/src/swap/auth/AuthService.java new file mode 100644 index 0000000..2ed232a --- /dev/null +++ b/src/swap/auth/AuthService.java @@ -0,0 +1,25 @@ +package swap.auth; + +import java.time.Instant; + +public abstract class AuthService { + public abstract Session login(String username, String password); + public abstract Session validate(String sessionKey); + + protected Instant getInstant() { + return Instant.now(); + } + /* + protected boolean hasSessionId(String suggestedId) { + return validate(suggest) + } + + protected String newSessionId() { + + } + + protected Session newSession(String username, int seconds) { + Instant tim = getInstant(); + Instant exp = tim.plusSeconds(seconds); + }*/ +} diff --git a/src/swap/auth/BasicCrypto.java b/src/swap/auth/BasicCrypto.java new file mode 100644 index 0000000..0578708 --- /dev/null +++ b/src/swap/auth/BasicCrypto.java @@ -0,0 +1,67 @@ +package swap.auth; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +public class BasicCrypto { + public static int simpleRandom() { + SecureRandom r = new SecureRandom(); + return r.nextInt(); + } + + public static String simpleRandomHex() { + SecureRandom r = new SecureRandom(); + byte[] b = new byte[16]; + return hex(b); + } + + public static byte[] binarySHA256(byte[] input) { + MessageDigest d; + try { + d = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new Error("System encryption problem", e); + } + return d.digest(input); + } + + public static byte[] binarySHA256(String input) { + return binarySHA256(byt(input)); + } + + public static String hexSHA256(byte[] input) { + return hex(binarySHA256(input)); + } + + public static String hexSHA256(String input) { + return hexSHA256(byt(input)); + } + + public static byte[] byt(String s) { + if (s == null) { + return new byte[0]; + } + try { + return s.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new Error("Encoding problem", e); + } + } + + public static String hex(byte[] input) { + StringBuffer result = new StringBuffer(); + + for (int i = 0; i < input.length; i++) { + String h = Integer.toHexString(input[i] & 0xFF); + if(h.length() < 2) { + result.append('0' + h); + } else { + result.append(h); + } + } + + return result.toString(); + } +} diff --git a/src/swap/auth/Session.java b/src/swap/auth/Session.java new file mode 100644 index 0000000..8ae5456 --- /dev/null +++ b/src/swap/auth/Session.java @@ -0,0 +1,20 @@ +package swap.auth; + +import java.time.Instant; + +public class Session { + private final AuthService service; + private final String username; + private final String sessionKey; + private final Instant timestamp; + private final Instant expiry; + + Session(AuthService service, String username, String sessionKey, Instant timestamp, Instant expiry) { + this.service = service; + this.username = username; + this.sessionKey = sessionKey; + this.timestamp = timestamp; + this.expiry = expiry; + + } +} diff --git a/src/swap/data/DataAbstraction.java b/src/swap/data/DataAbstraction.java new file mode 100644 index 0000000..8427148 --- /dev/null +++ b/src/swap/data/DataAbstraction.java @@ -0,0 +1,71 @@ +package swap.data; + +public abstract class DataAbstraction { + public abstract DataString[] getKeys(); + public abstract DataString getValue(DataString key); + public abstract void setValue(DataString key, DataString value); + public abstract void delete(DataString key); + + public boolean has(DataString key) { + for (DataString k: getKeys()) { + if (k.equals(key)) { + return true; + } + } + return false; + } + + public boolean has(String key) { + return has(DataString.ofUTF8(key)); + } + + public final String getUTF8(DataString key) { + DataString r = getValue(key); + if (r == null) { + return null; + } else { + return r.getUTF8(); + } + } + + public final DataObject getDataObject(DataString key) { + DataString r = getValue(key); + if (r == null) { + return null; + } else { + return DataObject.decodeSimple(r); + } + } + + public final DataString getValue(String key) { + return getValue(DataString.ofUTF8(key)); + } + + public final String getUTF8(String key) { + return getUTF8(DataString.ofUTF8(key)); + } + + public final DataObject getDataObject(String key) { + return getDataObject(DataString.ofUTF8(key)); + } + + public final void setValue(String key, DataString value) { + setValue(DataString.ofUTF8(key), value); + } + + public final void setValue(DataString key, String value) { + setValue(key, DataString.ofUTF8(value)); + } + + public final void setValue(String key, String value) { + setValue(DataString.ofUTF8(key), DataString.ofUTF8(value)); + } + + public final void setValue(String key, DataObject value) { + setValue(DataString.ofUTF8(key), DataObject.encodeSimple(value)); + } + + public final void setValue(DataString key, DataObject value) { + setValue(key, DataObject.encodeSimple(value)); + } +} diff --git a/src/swap/data/DataConv.java b/src/swap/data/DataConv.java new file mode 100644 index 0000000..6ad5566 --- /dev/null +++ b/src/swap/data/DataConv.java @@ -0,0 +1,115 @@ +package swap.data; + +import java.util.Map; + +import swap.util.JSON; + +public class DataConv { + + public DataConv() { + // TODO Auto-generated constructor stub + } + + public static DataObject toDataObject(Object o) { + if (o == null) { + return null; + } else if (o instanceof JSON) { + return toDataObject((JSON) o); + } else if (o instanceof Boolean) { + return DataObject.of(((Boolean) o).booleanValue()); + } else if (o instanceof Integer) { + return DataObject.of(((Integer) o).intValue()); + } else if (o instanceof Double) { + return DataObject.of(((Double) o).doubleValue()); + } else if (o instanceof String) { + return DataObject.of((String) o); + } else { + throw new Error("Not sure how to convert objects of this class: " + o.getClass() + ""); + } + } + + public static DataObject toDataObject(JSON o) { + switch(o.type_get()) { + case NULL: + return null; + case FALSE: + return DataObject.FALSE; + case TRUE: + return DataObject.TRUE; + case JSON_STRING: + return DataObject.of(o.rawValue().toString()); + case JSON_NUMBER: + return toDataObject(o.rawValue()); + case JSON_ARRAY: { + DataObject[] d = new DataObject[o.json_size()]; + for (int i = 0; i < d.length; i++) { + d[i] = toDataObject(o.json_get(i)); + } + return new DataObject.List(d); + } + case JSON_OBJECT: { + DataObject.Map m = new DataObject.Map(); + for (Object xo: o) { + Map.Entry x = (Map.Entry)xo; + m.set(DataObject.of(x.getKey()), toDataObject(x.getValue())); + } + return m; + } + default: + throw new Error("Don't know hot to decode JSON objects of type: " + o.type_get()); + } + } + + public static JSON toJSON(DataObject o) { + switch (DataObject.typeOf(o)) { + case NULL: + return new JSON("null"); + case BOOL: + return new JSON((((DataObject.Bool) o).value) ? "true" : "false"); + case I32: + return new JSON("" + (((DataObject.I32)o).value)); + case F64: + return new JSON("" + (((DataObject.F64)o).value)); + case TEXT: + return JSON.rawString(((DataObject.Text)o).stringValue()); + case LIST: { + String s = "["; + for (int i = 0; i < ((DataObject.List)o).count(); i++) { + if (i != 0) { + s += ","; + } + s += toJSON(((DataObject.List)o).get(i)).toString(); + } + s += "]"; + return new JSON(s); + } + case MAP: { + String s = "{"; + DataObject.Text[] keys = ((DataObject.Map)o).keys(); + for (int i = 0; i < keys.length; i++) { + if (i != 0) { + s += ","; + } + s += toJSON(keys[i]).toString() + ":"; + s += toJSON(((DataObject.Map)o).get(keys[i])).toString(); + } + s += "}"; + return new JSON(s); + } + default: + throw new Error("Unrecognised object type: " + DataObject.typeOf(o)); + } + } + + // A simple test program + public static void main(String[] args) { + String test = "33";//"{\"foo\":\"bar\",\"baz\":[1,2,3]}"; + JSON j = new JSON(test); + System.out.println(j); + DataObject d = toDataObject(j); + DataString s = DataObject.encodeSimple(d); + DataObject d2 = DataObject.decodeSimple(s); + JSON j2 = toJSON(d2); + System.out.println(j2); + } +} diff --git a/src/swap/data/DataFile.java b/src/swap/data/DataFile.java new file mode 100644 index 0000000..26a4c80 --- /dev/null +++ b/src/swap/data/DataFile.java @@ -0,0 +1,317 @@ +package swap.data; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; + +public class DataFile extends DataSet { + private final File file; + + public DataFile(File file) { + this.file = file; + } + + public String getFilename() { + return file.getPath(); + } + + public String getLockFilename() { + String f = getFilename(); + if (f.endsWith(".db")) { + f = f.substring(0, f.length() - 3); + } + return f + ".lock"; + } + + public String getBackupFilename() { + String f = getFilename(); + if (f.endsWith(".db")) { + f = f.substring(0, f.length() - 3); + } + return f + ".backup"; + } + + /** + * This deletes the main database file from disk, as well as any lock files or backup files which have been automatically + * created for that file. If the database does not exist, the only effect should be temporarily creating the lock file and + * (after locking) removing it again. Care should be taken to avoid placing ".db" files in directories containing any other + * ".lock" or ".backup" + * files that might be accidentally deleted (or overwritten) when mistaken for files associated with a database of the + * same name. + */ + public void deleteAll() { + try { + doWithLock(getLockFilename(), new Runnable() { + + @Override + public void run() { + new File(getFilename()).delete(); + new File(getBackupFilename()).delete(); + } + }); + } catch (IOException e) { + throw new Error("Filed to delete database files", e); + } + new File(getLockFilename()).delete(); + } + + public static int loadInt(InputStream input) throws IOException { + int a = input.read(); + int b = input.read(); + int c = input.read(); + int d = input.read(); + if (a < 0 || b < 0 || c < 0 || d < 0) { + throw new IOException("Stream ended early, expecting a 32-bit int"); + } + return a | (b << 8) | (c << 16) | (d << 24); + } + + public static byte[] loadBytes(InputStream input, int nbytes) throws IOException { + byte[] b = new byte[nbytes]; + for (int i = 0; i < nbytes; i++) { + int x = input.read(); + if (x < 0) { + throw new IOException("Stream ended early, expected " + nbytes + " bytes but stopped at " + i); + } + b[i] = (byte) x; + } + return b; + } + + public static DataString loadByteString(InputStream input, int nbytes) throws IOException { + return DataString.ofBytes(loadBytes(input, nbytes)); + } + + public static DataString loadByteString(InputStream input) throws IOException { + int len = loadInt(input); + return loadByteString(input, len); + } + + public static String loadString(InputStream input, int nbytes) throws IOException { + return new String(loadBytes(input, nbytes), "UTF-8"); + } + + public static String loadString(InputStream input) throws IOException { + int len = loadInt(input); + return loadString(input, len); + } + + public static HashMap load(InputStream input) throws IOException { + HashMap result = new HashMap(); + String magic = loadString(input); + if (!magic.equals("SimpleDB")) { + throw new IOException("Bad file, expecting SimpleDB format"); + } + int ver = loadInt(input); + if (ver != 2) { + throw new IOException("Bad SimpleDB file, expecting version 2 but got " + ver); + } + int count = loadInt(input); + if (count < 0) { + throw new IOException("Bad SimpleDB file, invalid number of records: " + count); + } + int hash = loadInt(input); + for (int i = 0; i < count; i++) { + DataString k = loadByteString(input); + DataString v = loadByteString(input); + if (result.containsKey(k)) { + throw new IOException("Bad SimpleDB file, key '" + k + "' is repeated"); + } + result.put(k, v); + } + String endMagic = loadString(input); + if (!endMagic.equals("DBEnd")) { + throw new IOException("Bad SimpleDB file, end marker mismatch"); + } + int realHash = dataHash(result); + //System.err.println("Recorded hash:\t0b" + Integer.toBinaryString(hash)); + //System.err.println("Resulting hash:\t0b" + Integer.toBinaryString(realHash)); + if (hash != realHash) { + throw new IOException("Bad SimpleDB file, hash mismatch"); + } + return result; + } + + public static HashMap loadOrInitialise(File f) throws IOException { + if (!f.exists()) { + store(f, new HashMap()); + } + return load(f); + } + + public static HashMap load(File f) throws IOException { + FileInputStream i = new FileInputStream(f); + try { + return load(i); + } finally { + i.close(); + } + } + + public static void storeInt(OutputStream output, int value) throws IOException { + output.write(value & 0xFF); + output.write((value >> 8) & 0xFF); + output.write((value >> 16) & 0xFF); + output.write((value >> 24) & 0xFF); + } + + public static void storeBytes(OutputStream output, byte[] bytes) throws IOException { + storeInt(output, bytes.length); + for (int i = 0; i < bytes.length; i++) { + output.write(((int)bytes[i]) & 0xFF); + } + } + + public static void storeByteString(OutputStream output, DataString str) throws IOException { + storeBytes(output, str.getBytes()); + } + + public static void storeString(OutputStream output, String str) throws IOException { + storeBytes(output, str.getBytes("UTF-8")); + } + + public static void store(OutputStream output, HashMap data) throws IOException { + storeString(output, "SimpleDB"); + storeInt(output, 2); // Version + ArrayList keys = sortedKeys(data); + storeInt(output, keys.size()); + storeInt(output, dataHash(data)); + for (DataString k: keys) { + storeByteString(output, k); + storeByteString(output, data.get(k)); + } + storeString(output, "DBEnd"); + } + + public static ArrayList sortedKeys(HashMap data) { + ArrayList keys = new ArrayList(); + keys.addAll(data.keySet()); + keys.sort(new Comparator() { + @Override + public int compare(DataString o1, DataString o2) { + /*int q = o1.hashCode() - o2.hashCode(); + if (q == 0) { + System.err.println("HASH CONFLICT"); + return o1.compareTo(o2); + } else if (q < 0) { + return -1; + } else { + return 1; + }*/ + return o1.compareTo(o2); + } + }); + return keys; + } + + public static int dataHash(HashMap data) { + ArrayList keys = sortedKeys(data); + //System.err.println("Calculating hash of " + keys.size() + " pairs"); + int h = DataString.intHash(keys.size()); + for (DataString k: keys) { + //System.err.println("k'" + k.getUTF8() + "'=v'" + data.get(k).getUTF8() + "'"); + h = DataString.intHash((Integer.rotateRight(h, 1) ^ k.hashCode()) ^ data.get(k).hashCode()); + } + //System.err.println("Calculated hash:\t0b" + Integer.toBinaryString(h)); + return h; + } + + public static void store(File f, HashMap data) throws IOException { + FileOutputStream o = new FileOutputStream(f); + try { + store(o, data); + } finally { + o.close(); + } + } + + public static void doWithLock(String lockfileName, Runnable r) throws IOException { + //System.err.println("Locking on '" + lockfileName+ "'"); + File lf = new File(lockfileName); + if (!lf.getParentFile().exists()) { + throw new IOException("Directory '" + lf.getParentFile() + "' doesn't exist"); + } + lf.createNewFile(); + RandomAccessFile raf = new RandomAccessFile(lf, "rw"); + + FileChannel ch = raf.getChannel(); + FileLock l = ch.lock(); + try { + r.run(); + } finally { + l.close(); + ch.close(); + raf.close(); + } + } + + @Override + public synchronized void doOp(DataOp op) { + try { + doWithLock(getLockFilename(), new Runnable() { + @Override + public void run() { + try { + //System.err.println("Transaction started"); + final HashMap data = loadOrInitialise(new File(getFilename())); + DataTransaction tr = new DataTransaction() { + @Override + public void setValue(DataString key, DataString value) { + checkOperational(); + data.put(key, value); + } + + @Override + public DataString getValue(DataString key) { + checkOperational(); + return data.get(key); + } + + @Override + public DataString[] getKeys() { + ArrayList l = sortedKeys(data); + return l.toArray(new DataString[l.size()]); + } + + @Override + public void delete(DataString key) { + data.remove(key); + } + }; + op.perform(tr); + if (tr.isFinished()) { + //System.err.println("Transaction finished"); + Files.copy(Paths.get(file.getPath()), Paths.get(getBackupFilename()), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); + try { + store(new File(getFilename()), data); + } catch (Throwable t) { + System.err.println("Write failed. Rolling back to backup."); + Files.copy(Paths.get(getBackupFilename()), Paths.get(file.getPath()), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); + } + } else if (tr.isCancelled()) { + //System.err.println("Transaction CANCELLED"); + } else { + //System.err.println("Transaction UNFINISHED"); + } + } catch (IOException e) { + throw new Error("Transaction FAILED", e); + } + } + }); + } catch (IOException e) { + throw new Error("Transaction FAILED", e); + } + } +} diff --git a/src/swap/data/DataObject.java b/src/swap/data/DataObject.java new file mode 100644 index 0000000..25f07a6 --- /dev/null +++ b/src/swap/data/DataObject.java @@ -0,0 +1,411 @@ +package swap.data; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; + +/** + * A simple system for holding/encoding/decoding structured data, similar to a binary JSON format. + * + * This is built on the DataString class (to provide basic byte/text handling incl. reliable ordering of hashmap keys). + * + * @author Zak + * + */ +public abstract class DataObject { + public static DataObject.Bool of(boolean value) { + return value ? TRUE : FALSE; + } + + public static DataObject.I32 of(int value) { + return new I32(value); + } + + public static DataObject.F64 of(double value) { + return new F64(value); + } + + public static Text of(DataString value) { + return new Text(value); + } + + public static DataObject.Text of(String value) { + return new Text(DataString.ofUTF8(value)); + } + + public static DataObject.Text of(byte[] value) { + return new Text(DataString.ofBytes(value)); + } + + //public static final Null NULL = new Null(); + public static final Bool FALSE = new Bool(false); + public static final Bool TRUE = new Bool(true); + + public static enum Type { + NULL, + BOOL, + TEXT, + LIST, + MAP, + I32, + F64 + } + + public DataObject() { + // TODO Auto-generated constructor stub + } + + public abstract Type getType(); + + public static Type typeOf(DataObject value) { + if (value == null) { + return Type.NULL; + } else { + return value.getType(); + } + } + + private static class Counter { + int x = 0; + } + + public static DataObject decodeSimple(DataString data) { + return decodeSimple(data, new Counter()); + } + + public static DataObject decodeSimple(DataString data, Counter counter) { + if (data.size() < 1) { + throw new Error("Object data terminated early"); + } + int first = data.get(0); + + //System.out.println("Decoding type " + first); + + data = data.sub(1); + counter.x++; + + switch (first) { + case 0: + return null; + case 1: + return FALSE; + case 2: + return TRUE; + case 3: + counter.x += 4; + return DataObject.of(decodeI32(data)); + case 4: + counter.x += 8; + return DataObject.of(decodeF64(data)); + case 5: { + int len = decodeI32(data); + if (len < 0) { + throw new Error("Invalid string length: " + len); + } + counter.x += 4 + len; + DataString dat = data.sub(4, 4 + len); + return new Text(dat); + } + case 6: { + int len = decodeI32(data); + if (len < 0) { + throw new Error("Invalid list length: " + len); + } + + counter.x += 4; + data = data.sub(4); + + DataObject[] values = new DataObject[len]; + + for (int i = 0; i < len; i++) { + int oldx = counter.x; + values[i] = decodeSimple(data, counter); + data = data.sub(counter.x - oldx); + } + + return new List(values); + } + case 7: { + int len = decodeI32(data); + if (len < 0) { + throw new Error("Invalid map length: " + len); + } + + counter.x += 4; + data = data.sub(4); + + Map m = new Map(); + + for (int i = 0; i < len; i++) { + int oldx = counter.x; + DataObject key = decodeSimple(data, counter); + data = data.sub(counter.x - oldx); + + oldx = counter.x; + DataObject value = decodeSimple(data, counter); + data = data.sub(counter.x - oldx); + + if (!(key instanceof Text)) { + throw new Error("Expected text key in map but got: " + key); + } + + //System.out.println("Got k=" + key + " v=" + value); + + m.set((Text) key, value); + } + + return m; + } + default: + throw new Error("Invalid object data type #" + first); + } + } + + public static int decodeI32(DataString data) { + if (data.size() < 4) { + throw new Error("Object data terminated early"); + } + return data.get(0) | (data.get(1) << 8) | (data.get(2) << 16) | (data.get(3) << 24); + } + + private static long decodeI64(DataString data) { + long lo = decodeI32(data) & 0xFFFFFFFFL; + long hi = decodeI32(data.sub(4)) & 0xFFFFFFFFL; + return lo | (hi << 32); + } + + public static double decodeF64(DataString data) { + return Double.longBitsToDouble(decodeI64(data)); + } + + public static DataString encodeSimple(DataObject obj) { + switch (typeOf(obj)) { + case NULL: + return DataString.ofBytes(new byte[] { 0 }); + case BOOL: + return DataString.ofBytes(new byte[] { (byte) (((Bool)obj).value ? 2 : 1) }); + case I32: + return DataString.ofBytes(new byte[] { 3 }).cat(encodeI32(((I32)obj).value)); + case F64: + return DataString.ofBytes(new byte[] { 4 }).cat(encodeF64(((F64)obj).value)); + case TEXT: + return DataString.ofBytes(new byte[] { 5 }).cat(encodeI32(((Text)obj).value.size())).cat(((Text)obj).value); + case LIST: + return DataString.ofBytes(new byte[] { 6 }).cat(encodeListInner((List) obj)); + case MAP: + return DataString.ofBytes(new byte[] { 7 }).cat(encodeMapInner((Map) obj)); + default: + throw new Error("Unrecognised object type: " + typeOf(obj)); + } + } + + private static DataString encodeMapInner(Map obj) { + DataString result = encodeI32(obj.count()); + Text[] keys = obj.keys(); + for (int i = 0; i < keys.length; i++) { + result = result.cat(encodeSimple(keys[i])).cat(encodeSimple(obj.get(keys[i]))); + } + return result; + } + + private static DataString encodeListInner(List obj) { + DataString result = encodeI32(obj.count()); + + for (int i = 0; i < obj.count(); i++) { + result = result.cat(encodeSimple(obj.get(i))); + } + + return result; + } + + public static DataString encodeI32(int value) { + return DataString.ofBytes(new byte[] { + (byte) value, + (byte) (value >> 8), + (byte) (value >> 16), + (byte) (value >> 24) + }); + } + + private static DataString encodeI64(long value) { + return encodeI32((int) value).cat(encodeI32((int) (value >> 32))); + } + + public static DataString encodeF64(double value) { + return encodeI64(Double.doubleToRawLongBits(value)); + } + + /*public static class Null extends DataObject { + + }*/ + + public static class Bool extends DataObject { + public final boolean value; + + Bool(boolean value) { + this.value = value; + } + + @Override + public Type getType() { + return Type.BOOL; + } + } + + public static class List extends DataObject { + private DataObject[] values; + + public List(DataObject... values) { + this.values = values.clone(); + } + + public int count() { + return values.length; + } + + public DataObject get(int idx) { + if (idx < 0 || idx >= values.length) { + return null; + } else { + return values[idx]; + } + } + + @Override + public Type getType() { + return Type.LIST; + } + } + + public static class Map extends DataObject { + private HashMap values = new HashMap(); + + public int count() { + return values.size(); + } + + public DataObject get(Text key) { + return values.get(key); + } + + public DataObject get(String key) { + return get(of(key)); + } + + public void set(Text key, DataObject value) { + values.put(key, value); + } + + public void set(String key, DataObject value) { + set(of(key), value); + } + + public void delete(Text key) { + values.remove(key); + } + + public void delete(String key) { + delete(of(key)); + } + + public boolean has(Text key) { + return values.containsKey(key); + } + + public boolean has(String key) { + return has(of(key)); + } + + public Text[] keys() { + ArrayList result = sortedKeys(values); + return result.toArray(new Text[result.size()]); + } + + private static ArrayList sortedKeys(HashMap data) { + ArrayList keys = new ArrayList(); + keys.addAll(data.keySet()); + keys.sort(new Comparator() { + @Override + public int compare(Text o1, Text o2) { + /*int q = o1.hashCode() - o2.hashCode(); + if (q == 0) { + System.err.println("HASH CONFLICT"); + return o1.compareTo(o2); + } else if (q < 0) { + return -1; + } else { + return 1; + }*/ + return o1.value.compareTo(o2.value); + } + }); + return keys; + } + + @Override + public Type getType() { + return Type.MAP; + } + } + + public static class I32 extends DataObject { + public final int value; + + public I32(int value) { + this.value = value; + } + + @Override + public Type getType() { + return Type.I32; + } + } + + public static class F64 extends DataObject { + public final double value; + + public F64(double value) { + this.value = value; + } + + @Override + public Type getType() { + return Type.F64; + } + } + + public static class Text extends DataObject { + public final DataString value; + + public Text(DataString value) { + this.value = value; + } + + public String stringValue() { + return value.getUTF8(); + } + + public byte[] bytesValue() { + return value.getBytes(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Text) { + return value.equals(((Text) obj).value); + } else { + return super.equals(obj); + } + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public Type getType() { + return Type.TEXT; + } + } +} diff --git a/src/swap/data/DataOp.java b/src/swap/data/DataOp.java new file mode 100644 index 0000000..d94bb80 --- /dev/null +++ b/src/swap/data/DataOp.java @@ -0,0 +1,5 @@ +package swap.data; + +public interface DataOp { + void perform(DataTransaction x); +} diff --git a/src/swap/data/DataSet.java b/src/swap/data/DataSet.java new file mode 100644 index 0000000..8277091 --- /dev/null +++ b/src/swap/data/DataSet.java @@ -0,0 +1,52 @@ +package swap.data; + +public abstract class DataSet extends DataAbstraction { + public abstract void doOp(DataOp op); + + @Override + public final DataString getValue(final DataString key) { + final DataVariable v = new DataVariable(); + doOp(new DataOp() { + @Override + public void perform(DataTransaction transaction) { + v.setValue(transaction.getValue(key)); + transaction.finish(); + } + }); + return v.getValue(); + } + + @Override + public final DataString[] getKeys() { + final DataVariable v = new DataVariable(); + doOp(new DataOp() { + @Override + public void perform(DataTransaction transaction) { + v.setValue(transaction.getKeys()); + transaction.finish(); + } + }); + return v.getValue(); + } + + @Override + public final void setValue(final DataString key, final DataString value) { + doOp(new DataOp() { + @Override + public void perform(DataTransaction transaction) { + transaction.setValue(key, value); + transaction.finish(); + } + }); + } + + @Override + public void delete(final DataString key) { + doOp(new DataOp() { + @Override + public void perform(DataTransaction x) { + x.delete(key); + } + }); + } +} diff --git a/src/swap/data/DataString.java b/src/swap/data/DataString.java new file mode 100644 index 0000000..067cf4e --- /dev/null +++ b/src/swap/data/DataString.java @@ -0,0 +1,139 @@ +package swap.data; + +import java.io.UnsupportedEncodingException; + +public final class DataString implements Comparable { + private final byte[] data; + + private DataString(byte[] copiedData) { + this.data = copiedData; + } + + public static DataString ofBytes(byte[] bytes) { + return new DataString(copy(bytes)); + } + + public static DataString ofUTF8(String string) { + try { + return new DataString(string.getBytes("UTF-8")); + } catch (UnsupportedEncodingException e) { + throw new Error("Encoding problem", e); + } + } + + public String getUTF8() { + try { + return new String(data, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new Error("Encoding problem", e); + } + } + + public byte[] getBytes() { + return copy(data); + } + + public int size() { + return data.length; + } + + /** + * Returns the value at the given index. + * @param index The byte index starting at 0. + * @return The value in the range 0-255, or -1 if index out of range. + */ + public int get(int index) { + if (index < 0 || index >= data.length) { + return -1; + } + return ((int) data[index]) & 0xFF; + } + + public static byte[] copy(byte[] data) { + byte[] copy = new byte[data.length]; + for (int i = 0; i < data.length; i++) { + copy[i] = data[i]; + } + return copy; + } + + @Override + public int compareTo(DataString o) { + for (int i = 0; i < size() || i < o.size(); i++) { + if (i >= size()) { + return -1; // o greater + } else if (i >= o.size()) { + return 1; // this greater + } else { + int x = get(i); + int y = o.get(i); + int diff = x - y; + if (diff != 0) { + return diff; + } + } + } + return 0; // equal + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj instanceof DataString) { + return this.compareTo((DataString) obj) == 0; + } else { + return super.equals(obj); + } + } + + @Override + public int hashCode() { + int h = intHash(data.length); + for (int i = 0; i < data.length; i++) { + h = intHash(Integer.rotateRight(h, 1) ^ (((int)data[i]) & 0xFF)); + } + return h; + } + + public static int intHash(int input) { + input += 91937; + input ^= (input << 9); + input ^= (input >>> 11); + return input; + } + + public DataString cat(DataString tail) { + byte[] x = new byte[size() + tail.size()]; + for (int i = 0; i < size(); i++) { + x[i] = data[i]; + } + for (int i = 0; i < tail.size(); i++) { + x[data.length + i] = tail.data[i]; + } + return new DataString(x); + } + + public DataString sub(int firstIndex) { + return sub(firstIndex, size()); + } + + public DataString sub(int firstIndex, int nextIndex) { + if (firstIndex < 0) { + firstIndex = 0; + } + if (nextIndex > data.length) { + nextIndex = data.length; + } + if (nextIndex <= firstIndex) { + return new DataString(new byte[0]); + } + byte[] x = new byte[nextIndex - firstIndex]; + + for (int i = 0; i < x.length; i++) { + x[i] = data[firstIndex + i]; + } + + return new DataString(x); + } +} diff --git a/src/swap/data/DataTransaction.java b/src/swap/data/DataTransaction.java new file mode 100644 index 0000000..eb561cd --- /dev/null +++ b/src/swap/data/DataTransaction.java @@ -0,0 +1,34 @@ +package swap.data; + +public abstract class DataTransaction extends DataAbstraction { + private boolean finished = false; + private boolean cancelled = false; + + public void cancel() { + cancelled = true; + } + + public void finish() { + if (!cancelled) { + finished = true; + } + } + + public boolean isFinished() { + return finished && !cancelled; + } + + public boolean isCancelled() { + return cancelled; + } + + public boolean isOperational() { + return !(finished || cancelled); + } + + public void checkOperational() { + if (!isOperational()) { + throw new Error("This transaction is no longer operational"); + } + } +} diff --git a/src/swap/data/DataVariable.java b/src/swap/data/DataVariable.java new file mode 100644 index 0000000..34692a2 --- /dev/null +++ b/src/swap/data/DataVariable.java @@ -0,0 +1,21 @@ +package swap.data; + +public final class DataVariable { + private T value; + + public DataVariable() { + value = null; + } + + public DataVariable(T value) { + this.value = value; + } + + public T getValue() { + return value; + } + + public void setValue(T value) { + this.value = value; + } +} diff --git a/src/swap/dbtool/Main.java b/src/swap/dbtool/Main.java new file mode 100644 index 0000000..d491f03 --- /dev/null +++ b/src/swap/dbtool/Main.java @@ -0,0 +1,13 @@ +package swap.dbtool; + +public class Main { + + public static void main(String[] args) { + int i = 0; + + for (; i < args.length; i++) { + System.out.println("Arg #" + i + " = '" + args[i] + "'"); + } + } + +} diff --git a/src/swap/httpd/Content.java b/src/swap/httpd/Content.java new file mode 100644 index 0000000..258afc5 --- /dev/null +++ b/src/swap/httpd/Content.java @@ -0,0 +1,12 @@ +package swap.httpd; + +public interface Content { + /** + * Determines the content type. + * + * @return A suitable content type such as "text/html". + */ + public String getType(); + + public byte[] getPayload(); +} diff --git a/src/swap/httpd/Handler.java b/src/swap/httpd/Handler.java new file mode 100644 index 0000000..3fd8887 --- /dev/null +++ b/src/swap/httpd/Handler.java @@ -0,0 +1,5 @@ +package swap.httpd; + +public interface Handler { + Response handle(Request r); +} diff --git a/src/swap/httpd/Header.java b/src/swap/httpd/Header.java new file mode 100644 index 0000000..5d00ad1 --- /dev/null +++ b/src/swap/httpd/Header.java @@ -0,0 +1,19 @@ +package swap.httpd; + +public class Header { + private final String key; + private final String value; + + public Header(String key, String value) { + super(); + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + public String getValue() { + return value; + } +} diff --git a/src/swap/httpd/HeaderSet.java b/src/swap/httpd/HeaderSet.java new file mode 100644 index 0000000..d22bf88 --- /dev/null +++ b/src/swap/httpd/HeaderSet.java @@ -0,0 +1,41 @@ +package swap.httpd; + +import java.util.ArrayList; + +public abstract class HeaderSet { + + private final ArrayList
headers = new ArrayList
(); + + public void addHeader(Header header) { + headers.add(header); + } + + public void addHeader(String key, String value) { + addHeader(new Header(key, value)); + } + + public int countHeaders() { + return headers.size(); + } + + public Header getHeader(int i) { + if (i < 0 || i >= headers.size()) { + return null; + } else { + return headers.get(i); + } + } + + public Header getHeader(String key) { + for (int i = headers.size() - 1; i >= 0; i--) { + if (headers.get(i).getKey().equals(key)) { + return headers.get(i); + } + } + return null; + } + + public boolean hasHeader(String key) { + return getHeader(key) != null; + } +} diff --git a/src/swap/httpd/ListenerThread.java b/src/swap/httpd/ListenerThread.java new file mode 100644 index 0000000..b9d011b --- /dev/null +++ b/src/swap/httpd/ListenerThread.java @@ -0,0 +1,44 @@ +package swap.httpd; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; + +public class ListenerThread extends Thread { + private ServerGroup group; + private int port; + + public ListenerThread(ServerGroup group, int port) { + this.group = group; + this.port = port; + } + + @Override + public void run() { + try { + group.listenerStarted(this); + ServerSocket socket = createSocket(); + while (!socket.isClosed()) { + Socket s = socket.accept(); + group.handleNewConnection(this, s); + } + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } finally { + group.listenerStopped(this); + } + } + + protected ServerSocket createSocket() throws IOException { + return new ServerSocket(port); + } + + public ServerGroup getGroup() { + return group; + } + + public int getPort() { + return port; + } +} diff --git a/src/swap/httpd/LoggedServerGroup.java b/src/swap/httpd/LoggedServerGroup.java new file mode 100644 index 0000000..8f2a574 --- /dev/null +++ b/src/swap/httpd/LoggedServerGroup.java @@ -0,0 +1,60 @@ +package swap.httpd; + +import java.net.Socket; + +import swap.log.Log; +import swap.log.PrintStreamLog; + +public class LoggedServerGroup extends ServerGroup { + private final Log log; + + public LoggedServerGroup(Handler handler, Log log) { + super(handler); + this.log = log; + } + + public LoggedServerGroup(Handler handler) { + this(handler, new PrintStreamLog()); + } + + @Override + protected void doConnection(ListenerThread listenerThread, Socket s) { + try { + log.line("Starting connection", this, listenerThread, s); + super.doConnection(listenerThread, s); + } finally { + log.line("Finished connection", this, listenerThread, s); + } + } + + @Override + protected Response invokeHandler(Request r) { + Response result = null; + try { + log.line("Starting handler", this, r.getMethod(), r.getURL(), r.getProtocol()); + result = super.invokeHandler(r); + } finally { + log.line("Finished handler", this, result); + } + return result; + } + + @Override + public void startHTTP(int port) { + log.line("Attempting to start HTTP listener on port", port); + super.startHTTP(port); + } + + @Override + public synchronized void listenerStarted(ListenerThread listenerThread) { + log.line("Listener started", listenerThread); + super.listenerStarted(listenerThread); + } + + @Override + public synchronized void listenerStopped(ListenerThread listenerThread) { + log.line("Listener stopped", listenerThread); + super.listenerStopped(listenerThread); + } + +} diff --git a/src/swap/httpd/Request.java b/src/swap/httpd/Request.java new file mode 100644 index 0000000..e51c68b --- /dev/null +++ b/src/swap/httpd/Request.java @@ -0,0 +1,37 @@ +package swap.httpd; + +import java.util.ArrayList; + +public class Request extends HeaderSet { + private final ListenerThread listener; + private final String method, url, protocol; + private byte[] putData; + + public Request(ListenerThread listener, String method, String url, String protocol) { + this.listener = listener; + this.method = method; + this.url = url; + this.protocol = protocol; + } + + public ListenerThread getListener() { + return listener; + } + public String getMethod() { + return method; + } + public String getURL() { + return url; + } + public String getProtocol() { + return protocol; + } + + public void setPutData(byte[] data) { + this.putData = data; + } + + public byte[] getPutData() { + return putData; + } +} diff --git a/src/swap/httpd/Response.java b/src/swap/httpd/Response.java new file mode 100644 index 0000000..4d33f9a --- /dev/null +++ b/src/swap/httpd/Response.java @@ -0,0 +1,81 @@ +package swap.httpd; + +import java.io.UnsupportedEncodingException; + +public class Response extends HeaderSet { + private final String protocol; + private final int code; + private final String desc; + + private byte[] payload = null; + + public static final int OK = 200; + public static final int ERROR = 400; + public static final int UNAUTHORISED = 401; + public static final int NOTFOUND = 404; + + public Response(String protocol, int code, String desc) { + this.protocol = protocol; + this.code = code; + this.desc = desc; + } + + public Response(String protocol, int code) { + this(protocol, code, defaultCodeDesc(code)); + } + + public Response(int code) { + this("HTTP/1.1", code); + } + + public String getProtocol() { + return protocol; + } + + public int getCode() { + return code; + } + + public String getDesc() { + return desc; + } + + public byte[] getPayload() { + return payload; + } + + public static String defaultCodeDesc(int code) { + switch (code) { + case OK: + return "OK"; + case ERROR: + return "ERROR"; + case UNAUTHORISED: + return "UNAUTHORISED"; + case NOTFOUND: + return "NOTFOUND"; + default: + return "Unknown"; + } + } + + public void setContent(Content content) { + if (!hasHeader("Content-Type")) { + addHeader("Content-Type", content.getType()); + } + setContent(content.getPayload()); + } + + public void setContent(String string) { + try { + setContent(string.getBytes("UTF-8")); + } catch (UnsupportedEncodingException e) { + throw new Error("Encoding problem", e); + } + } + + public void setContent(byte[] bytes) { + addHeader("Content-Length", "" + bytes.length); + payload = bytes; + } +} diff --git a/src/swap/httpd/SecureListenerThread.java b/src/swap/httpd/SecureListenerThread.java new file mode 100644 index 0000000..649c7af --- /dev/null +++ b/src/swap/httpd/SecureListenerThread.java @@ -0,0 +1,64 @@ +package swap.httpd; + +import java.io.FileInputStream; +import java.io.IOException; +import java.net.ServerSocket; +import java.security.KeyStore; + +import javax.net.ServerSocketFactory; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocketFactory; + +/** + * To create a new keystore try "keytool -genkey -alias hosttls -keystore testkeys -keyalg RSA" then "keytool -importkeystore -srckeystore testkeys -destkeystore testkeys -deststoretype pkcs12". + * @author Zak + * + */ +public class SecureListenerThread extends ListenerThread { + + private final String keystoreName; + private final String passPhrase; + + public SecureListenerThread(ServerGroup group, int port, String keystoreName, String passPhrase) { + super(group, port); + this.keystoreName = keystoreName; + this.passPhrase = passPhrase; + } + + @Override + protected ServerSocket createSocket() throws IOException { + ServerSocketFactory f = getServerSocketFactory("TLS", keystoreName, passPhrase/*, alias*/); + return f.createServerSocket(getPort()); + } + + // Based on example code at https://docs.oracle.com/javase/10/security/sample-code-illustrating-secure-socket-connection-client-and-server.htm#JSSEC-GUID-3561ED02-174C-4E65-8BB1-5995E9B7282C + private static ServerSocketFactory getServerSocketFactory(String type, String keystoreName, String passPhrase/*, String alias*/) { + if (type.equals("TLS")) { + SSLServerSocketFactory ssf = null; + try { + // set up key manager to do server authentication + SSLContext ctx; + KeyManagerFactory kmf; + KeyStore ks; + char[] passphrase = passPhrase.toCharArray(); + + ctx = SSLContext.getInstance("TLS"); + kmf = KeyManagerFactory.getInstance("SunX509"); + ks = KeyStore.getInstance("PKCS12"/*"JKS"*/); + + ks.load(new FileInputStream(keystoreName), passphrase); + kmf.init(ks, passphrase); + ctx.init(kmf.getKeyManagers(), null, null); + + ssf = ctx.getServerSocketFactory(); + return ssf; + } catch (Exception e) { + e.printStackTrace(); + } + } else { + return ServerSocketFactory.getDefault(); + } + return null; + } +} diff --git a/src/swap/httpd/ServerGroup.java b/src/swap/httpd/ServerGroup.java new file mode 100644 index 0000000..7cc813c --- /dev/null +++ b/src/swap/httpd/ServerGroup.java @@ -0,0 +1,150 @@ +package swap.httpd; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.Socket; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class ServerGroup { + + private final ExecutorService executor; + private final Handler handler; + + public ServerGroup(Handler handler) { + executor = Executors.newFixedThreadPool(32); + this.handler = handler; + } + + public void startHTTP(int port) { + ListenerThread t = new ListenerThread(this, port); + t.start(); + } + + public synchronized void handleNewConnection(final ListenerThread listenerThread, final Socket s) { + executor.execute(new Runnable() { + @Override + public void run() { + doConnection(listenerThread, s); + } + }); + } + + public static String readHTTPLine(Socket s) throws IOException { + String r = ""; + boolean wasCR = false; + boolean wasLF = false; + while (!wasLF) { + int c = s.getInputStream().read(); + if (c < 0) { + throw new IOException("Socket closed early?"); + } + if (c == '\r') { + wasCR = true; + wasLF = false; + } else if (c == '\n') { + if (wasCR) { + return r; + } else { + //throw new Error("Bad sequence, LF with no CR"); + return r; + } + } else { + wasCR = false; + wasLF = false; + r += Character.toString((char) c); + } + } + //System.err.println("GOT LINE '" + r + "'"); + //Unreachable? + return r; + } + + public static byte[] readBytes(Socket s, int n) throws IOException { + byte[] result = new byte[n]; + + for (int i = 0; i < result.length; i++) { + result[i] = (byte) s.getInputStream().read(); + } + + return result; + } + + protected void doConnection(ListenerThread listenerThread, Socket s) { + try { + String firstLine = readHTTPLine(s); + String[] firstLineParts = firstLine.split(" "); + if (firstLineParts.length != 3) { + throw new Error("Bad first line of HTTP request, expecting 3 parts"); + } + if (!firstLineParts[2].equals("HTTP/1.1")) { + throw new Error("Bad first line of HTTP request, expecting HTTP/1.1"); + } + Request r = new Request(listenerThread, firstLineParts[0], firstLineParts[1], firstLineParts[2]); + + String l = null; + while (!(l = readHTTPLine(s)).equals("")) { + String[] lParts = l.split(": "); + if (lParts.length != 2) { + throw new Error("Bad HTTP header line, expected ': ' in the middle"); + } + //System.err.println("GOT HEADER " + lParts[0]); + r.addHeader(new Header(lParts[0], lParts[1])); + } + + if (r.getMethod().equals("PUT")) { + // TODO: Error checking when decoding header? + String lenstr = r.getHeader("Content-Length").getValue(); + int len = Integer.parseInt(lenstr); + byte[] data = readBytes(s, len); + r.setPutData(data); + } + + Response res = invokeHandler(r); + writeResponse(s, res); + } catch (Exception e) { + try { + writeResponse(s, new Response(400)); + } catch (Exception ee) { + ee.printStackTrace(); + } + } finally { + try { + s.close(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } + + protected Response invokeHandler(Request r) { + return handler.handle(r); + } + + private void writeResponse(Socket s, Response r) throws IOException { + try { + s.getOutputStream().write((r.getProtocol() + " " + r.getCode() + " " + r.getDesc() + "\r\n").getBytes("UTF-8")); + for (int i = 0; i < r.countHeaders(); i++) { + Header h = r.getHeader(i); + s.getOutputStream().write((h.getKey() + ": " + h.getValue() + "\r\n").getBytes("UTF-8")); + } + s.getOutputStream().write(("\r\n").getBytes("UTF-8")); + if (r.getPayload() != null) { + s.getOutputStream().write(r.getPayload()); + } + } catch (UnsupportedEncodingException e) { + throw new Error("Encoding problem", e); + } + } + + public synchronized void listenerStarted(ListenerThread listenerThread) { + + } + + public synchronized void listenerStopped(ListenerThread listenerThread) { + + } + + +} diff --git a/src/swap/library/LibraryLogin.java b/src/swap/library/LibraryLogin.java new file mode 100644 index 0000000..750b668 --- /dev/null +++ b/src/swap/library/LibraryLogin.java @@ -0,0 +1,54 @@ +package swap.library; + +import swap.auth.BasicCrypto; +import swap.data.DataObject.Map; +import swap.superdata.SuperDoc; +import swap.data.DataObject; +import swap.data.DataString; + +public class LibraryLogin extends LibraryShelf { + + public LibraryLogin(LibrarySet library, DataString title, Map config) { + super(library, title, config); + } + + @Override + protected void doInitialSetup() { + SuperDoc doc = set.request(DataString.ofUTF8("admin"), null, null); + if (!doc.isNew()) { + throw new Error("Already setup"); + } + String pw = BasicCrypto.simpleRandomHex(); + doc.contents = DataObject.encodeSimple(generateSalty(pw)); + System.out.println("Your initial admin password is '" + pw + "'"); + } + + public static DataObject.Map generateSalty(String pw) { + DataObject.Map result = new DataObject.Map(); + + String salt = BasicCrypto.simpleRandomHex(); + result.set("salt", DataObject.of(salt)); + result.set("pass", DataObject.of(BasicCrypto.hexSHA256(pw + salt))); + + return result; + } + + public static boolean validateSalty(DataObject o, String pw) { + if (!(o instanceof DataObject.Map)) { + throw new Error("Bad login data"); + } + DataObject.Map m = (DataObject.Map) o; + DataObject p = m.get("pass"); + if (!(p instanceof DataObject.Text)) { + throw new Error("Bad login data"); + } + String pass = ((DataObject.Text) p).stringValue(); + DataObject s = m.get("salt"); + if (!(s instanceof DataObject.Text)) { + throw new Error("Bad login data"); + } + String salt = ((DataObject.Text) s).stringValue(); + + return BasicCrypto.hexSHA256(pw + salt).equals(pass); + } +} diff --git a/src/swap/library/LibrarySection.java b/src/swap/library/LibrarySection.java new file mode 100644 index 0000000..729770d --- /dev/null +++ b/src/swap/library/LibrarySection.java @@ -0,0 +1,18 @@ +package swap.library; + +import swap.data.DataObject; +import swap.data.DataString; + +public abstract class LibrarySection { + public final LibrarySet library; + public final DataString title; + + public LibrarySection(LibrarySet library, DataString title, DataObject.Map config) { + this.library = library; + this.title = title; + } + + protected void doInitialSetup() { + + } +} diff --git a/src/swap/library/LibrarySet.java b/src/swap/library/LibrarySet.java new file mode 100644 index 0000000..276e638 --- /dev/null +++ b/src/swap/library/LibrarySet.java @@ -0,0 +1,98 @@ +package swap.library; + +import java.io.File; +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; + +import swap.data.DataFile; +import swap.data.DataObject; +import swap.data.DataOp; +import swap.data.DataString; +import swap.data.DataTransaction; +import swap.data.DataObject.Map; +import swap.superdata.SuperBin; +import swap.superdata.SuperLock; +import swap.superdata.SuperSet; + +public class LibrarySet { + private final String pathBase; + public final SuperLock lock; + private DataFile configDB; + private HashMap sections = new HashMap(); + + public LibrarySet(SuperLock lock, File path) { + this.lock = lock; + path = path.getAbsoluteFile(); + if (!path.exists()) { + path.mkdirs(); + } + if (!path.isDirectory()) { + throw new Error("LibrarySet path '" + path + "' is not a directory!"); + } + this.pathBase = path.getAbsolutePath(); + configDB = lock.open(new File(path.getPath() + "/config.db")); + + configDB.doOp(new DataOp() { + + @Override + public void perform(DataTransaction x) { + boolean setup = false; + if (x.getUTF8("_login") == null) { + setup = true; + x.setValue("_login", generateNewLogin()); + } + + DataString[] keys = x.getKeys(); + for (DataString k: keys) { + DataObject o = x.getDataObject(k); + if (!(o instanceof DataObject.Map)) { + throw new Error("Data in wrong format"); + } + DataObject.Map m = (DataObject.Map) o; + DataObject t = m.get("type"); + if (!(t instanceof DataObject.Text)) { + throw new Error("Data in wrong format"); + } + LibrarySection s; + sections.put(k, s = instantiateSection(((DataObject.Text) t).value, m)); + if (setup) { + s.doInitialSetup(); + } + } + + x.finish(); + } + }); + } + + protected LibrarySection instantiateSection(DataString name, DataObject.Map config) { + String n = name.getUTF8(); + try { + Class cl = (Class) this.getClass().getClassLoader().loadClass(n); + return cl.getConstructor(LibrarySet.class, DataString.class, DataObject.Map.class).newInstance(this, name, config); + } catch (Exception e) { + throw new Error("Failed to instantiate section '" + n + "'"); + } + } + + public String defaultSectionPath(String name) { + return pathBase + "/" + name; + } + + protected DataObject.Map generateNewLogin() { + DataObject.Map result = new DataObject.Map(); + + result.set("type", typeText(LibraryLogin.class)); + result.set("path", DataObject.of(defaultSectionPath("_login"))); + + return result; + } + + public static DataObject.Text typeText(Class c) { + return DataObject.of(typeName(c)); + } + + public static DataString typeName(Class c) { + return DataString.ofUTF8(c.getName()); + } +} diff --git a/src/swap/library/LibraryShelf.java b/src/swap/library/LibraryShelf.java new file mode 100644 index 0000000..9323da7 --- /dev/null +++ b/src/swap/library/LibraryShelf.java @@ -0,0 +1,23 @@ +package swap.library; + +import java.io.File; + +import swap.data.DataObject; +import swap.data.DataObject.Map; +import swap.data.DataString; +import swap.superdata.SuperSet; + +public class LibraryShelf extends LibrarySection { + public final SuperSet set; + + public LibraryShelf(LibrarySet library, DataString title, Map config) { + super(library, title, config); + DataObject t = config.get("path"); + if (!(t instanceof DataObject.Text)) { + throw new Error("Data in wrong format"); + } + String path = ((DataObject.Text)t).stringValue(); + set = new SuperSet(library.lock, new File(path)); + } + +} diff --git a/src/swap/log/Log.java b/src/swap/log/Log.java new file mode 100644 index 0000000..c63585d --- /dev/null +++ b/src/swap/log/Log.java @@ -0,0 +1,74 @@ +package swap.log; + +import java.time.Instant; + +public abstract class Log { + static final int LOW = 0; + static final int HINT = 3; + static final int DEFAULT = 4; + static final int WARNING = 5; + static final int ERROR = 7; + static final int HIGH = 10; + + public static int saneLevel(int level) { + if (level < LOW) { + return LOW; + } else if (level > HIGH) { + return HIGH; + } else { + return level; + } + } + + public static String stringLevel(int level) { + level = saneLevel(level); + switch(level) { + case LOW: + return "LOW"; + case HINT: + return "HINT"; + case DEFAULT: + return "DEFAULT"; + case WARNING: + return "WARNING"; + case ERROR: + return "ERROR"; + case HIGH: + return "HIGH"; + default: + return "LEVEL " + level; + } + } + + public static String stringLine(Object[] line) { + String r = ""; + boolean first = true; + for (Object o: line) { + if (first) { + first = false; + } else { + r += " "; + } + r += o; + } + return r; + } + + public static String stringThread() { + return Thread.currentThread().toString(); + } + + public static String stringTime() { + return Instant.now().toString(); + } + + protected abstract void doWriteLine(int saneLevel, Object[] line); + + public synchronized final void writeLine(int level, Object...line) { + doWriteLine(saneLevel(level), line); + } + + public synchronized final void line(Object...line) { + doWriteLine(DEFAULT, line); + } +} diff --git a/src/swap/log/PrintStreamLog.java b/src/swap/log/PrintStreamLog.java new file mode 100644 index 0000000..192a77b --- /dev/null +++ b/src/swap/log/PrintStreamLog.java @@ -0,0 +1,27 @@ +package swap.log; + +import java.io.PrintStream; +import java.time.Instant; + +public class PrintStreamLog extends Log { + private final PrintStream target; + + public PrintStreamLog(PrintStream target) { + super(); + this.target = target; + } + + public PrintStreamLog() { + this(System.err); + } + + public PrintStream getTarget() { + return target; + } + + @Override + protected void doWriteLine(int saneLevel, Object[] line) { + target.println(stringTime() + " " + stringLevel(saneLevel) + " in " + stringThread() + ":\t" + stringLine(line)); + } + +} diff --git a/src/swap/old/othercereal/Field.java b/src/swap/old/othercereal/Field.java new file mode 100644 index 0000000..f135c01 --- /dev/null +++ b/src/swap/old/othercereal/Field.java @@ -0,0 +1,9 @@ +package swap.old.othercereal; + +public class Field { + + public Field() { + // TODO Auto-generated constructor stub + } + +} diff --git a/src/swap/old/othercereal/Image.java b/src/swap/old/othercereal/Image.java new file mode 100644 index 0000000..03983bb --- /dev/null +++ b/src/swap/old/othercereal/Image.java @@ -0,0 +1,9 @@ +package swap.old.othercereal; + +public class Image { + + public Image() { + // TODO Auto-generated constructor stub + } + +} diff --git a/src/swap/old/othercereal/Method.java b/src/swap/old/othercereal/Method.java new file mode 100644 index 0000000..241c55e --- /dev/null +++ b/src/swap/old/othercereal/Method.java @@ -0,0 +1,11 @@ +package swap.old.othercereal; + +public class Method { + Type type; + String name; + + public Method() { + // TODO Auto-generated constructor stub + } + +} diff --git a/src/swap/old/othercereal/ObjectSet.java b/src/swap/old/othercereal/ObjectSet.java new file mode 100644 index 0000000..0103cea --- /dev/null +++ b/src/swap/old/othercereal/ObjectSet.java @@ -0,0 +1,9 @@ +package swap.old.othercereal; + +public class ObjectSet { + + public ObjectSet() { + // TODO Auto-generated constructor stub + } + +} diff --git a/src/swap/old/othercereal/Type.java b/src/swap/old/othercereal/Type.java new file mode 100644 index 0000000..c970ecf --- /dev/null +++ b/src/swap/old/othercereal/Type.java @@ -0,0 +1,9 @@ +package swap.old.othercereal; + +public class Type { + + public Type() { + // TODO Auto-generated constructor stub + } + +} diff --git a/src/swap/old/othercereal/TypeSet.java b/src/swap/old/othercereal/TypeSet.java new file mode 100644 index 0000000..a6b995a --- /dev/null +++ b/src/swap/old/othercereal/TypeSet.java @@ -0,0 +1,13 @@ +package swap.old.othercereal; + +public class TypeSet { + private boolean sealed = false; + + public TypeSet() { + // TODO Auto-generated constructor stub + } + + public boolean isSealed() { + return sealed; + } +} diff --git a/src/swap/old/pojocereal/Boxer.java b/src/swap/old/pojocereal/Boxer.java new file mode 100644 index 0000000..28ae3de --- /dev/null +++ b/src/swap/old/pojocereal/Boxer.java @@ -0,0 +1,107 @@ +package swap.old.pojocereal; + +import java.io.UnsupportedEncodingException; +import java.util.HashMap; + +public abstract class Boxer { + private final Brand brand; + private int position = 0; + private HashMap stringPositions = new HashMap(); + private HashMap grainPositions = new HashMap(); + + public Boxer(Brand brand) { + this.brand = brand; + } + + public Brand getBrand() { + return brand; + } + + public void reset() { + position = 0; + stringPositions.clear(); + grainPositions.clear(); + } + + public int getCurrentPosition() { + return position; + } + + public abstract void writeBytes(byte[] b) throws CerealException; + + /** + * Writes a byte (passed as an int for convenience, only low 8 bits written). + * + * @param b The value to be written. + * @throws CerealException + */ + public final void writeByte(int b) throws CerealException { + byte[] tmp = new byte[] {(byte)(b & 0xFF)}; + writeBytes(tmp); + } + + public final void writeBoolean(boolean val) throws CerealException { + writeByte(val ? 0xFF : 0x00); + } + + public final void writeI8(byte val) throws CerealException { + writeByte(val); + } + + public final void writeI16(short val) throws CerealException { + writeByte(val); + writeByte(val>>8); + } + + public final void writeI32(int val) throws CerealException { + writeByte(val); + writeByte(val>>8); + writeByte(val>>16); + writeByte(val>>24); + } + + public final void writeI64(long val) throws CerealException { + writeI32((int) val); + writeI32((int) (val >> 32)); + } + + /** + * Writes a string (as a UTF-8 byte sequence, preceded by it's 32-bit length). + * @param val + * @return The position of the start of the string. + * @throws CerealException + */ + public final int writeRawUTF8(String val) throws CerealException { + try { + int pos = getCurrentPosition(); + byte[] b = val.getBytes("UTF-8"); + writeI32(b.length); + writeBytes(b); + return pos; // Returns position of start of string + } catch (UnsupportedEncodingException e) { + throw new CerealException("Encoding problem", e); + } + } + + public final int writeUTF8(String val) throws CerealException { + if (stringPositions.containsKey(val)) { + int p = stringPositions.get(val).intValue(); + writeI32(-(1 + p)); + return p; + } else { + int p = writeRawUTF8(val); + stringPositions.put(val, Integer.valueOf(p)); + return p; + } + } + + + + public void close() { + position = -1; + } + + public final boolean isClosed() { + return position < 0; + } +} diff --git a/src/swap/old/pojocereal/Brand.java b/src/swap/old/pojocereal/Brand.java new file mode 100644 index 0000000..95f29ba --- /dev/null +++ b/src/swap/old/pojocereal/Brand.java @@ -0,0 +1,30 @@ +package swap.old.pojocereal; + +import java.util.HashMap; + +public class Brand { + private HashMap, Type> classTypes = new HashMap, Type>(); + private HashMap, Type> namedTypes = new HashMap, Type>(); + + public Type typeOf(Grain g) throws CerealException { + return typeOfClass(g.getClass()); + } + + public final synchronized Type typeOfClass(Class c) throws CerealException { + if (classTypes.containsKey(c)) { + return classTypes.get(c); + } + + Type result = generateTypeDesc(c); + + if (result != null) { + classTypes.put(c, result); + } + + return result; + } + + public Type generateTypeDesc(Class c) throws CerealException { + return null; + } +} diff --git a/src/swap/old/pojocereal/CerealException.java b/src/swap/old/pojocereal/CerealException.java new file mode 100644 index 0000000..4f463a8 --- /dev/null +++ b/src/swap/old/pojocereal/CerealException.java @@ -0,0 +1,29 @@ +package swap.old.pojocereal; + +public class CerealException extends Exception { + + public CerealException() { + // TODO Auto-generated constructor stub + } + + public CerealException(String message) { + super(message); + // TODO Auto-generated constructor stub + } + + public CerealException(Throwable cause) { + super(cause); + // TODO Auto-generated constructor stub + } + + public CerealException(String message, Throwable cause) { + super(message, cause); + // TODO Auto-generated constructor stub + } + + public CerealException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + // TODO Auto-generated constructor stub + } + +} diff --git a/src/swap/old/pojocereal/Field.java b/src/swap/old/pojocereal/Field.java new file mode 100644 index 0000000..0b3a85b --- /dev/null +++ b/src/swap/old/pojocereal/Field.java @@ -0,0 +1,57 @@ +package swap.old.pojocereal; + +public class Field extends Ingredient { + public String type; + public String name; + + public Field() { + // TODO Auto-generated constructor stub + } + + public static enum Kind { + INVALID, + PRIMITIVE, + GRAIN, + ARRAY + } + + public static enum Primitive { + BOOL, + I8, + I16, + I32, + I64, + F32, + F64, + UTF8 + } + + public static boolean isArray(String type) { + if (type == null) { + return false; + } + return type.endsWith("[]"); + } + + public static String arrayMemberType(String type) { + if (isArray(type)) { + return type.substring(0, type.length() - 2); + } else { + return null; + } + } + + public static boolean isPrimitive(String type) { + if (type == null) { + return false; + } + return type.startsWith("."); + } + + public static boolean isGrain(String type) { + if (type == null) { + return false; + } + return !(isArray(type) || isPrimitive(type)); + } +} diff --git a/src/swap/old/pojocereal/Grain.java b/src/swap/old/pojocereal/Grain.java new file mode 100644 index 0000000..2fa1fcc --- /dev/null +++ b/src/swap/old/pojocereal/Grain.java @@ -0,0 +1,5 @@ +package swap.old.pojocereal; + +public abstract class Grain extends Ingredient { + +} diff --git a/src/swap/old/pojocereal/Ingredient.java b/src/swap/old/pojocereal/Ingredient.java new file mode 100644 index 0000000..b4951ea --- /dev/null +++ b/src/swap/old/pojocereal/Ingredient.java @@ -0,0 +1,9 @@ +package swap.old.pojocereal; + +public abstract class Ingredient { + + public Ingredient() { + // TODO Auto-generated constructor stub + } + +} diff --git a/src/swap/old/pojocereal/Type.java b/src/swap/old/pojocereal/Type.java new file mode 100644 index 0000000..5b8dfd8 --- /dev/null +++ b/src/swap/old/pojocereal/Type.java @@ -0,0 +1,13 @@ +package swap.old.pojocereal; + +public class Type extends Ingredient { + String name; + Type parent; + Field fields; + //Field[] fields; + + public Type() { + // TODO Auto-generated constructor stub + } + +} diff --git a/src/swap/superdata/SuperBin.java b/src/swap/superdata/SuperBin.java new file mode 100644 index 0000000..a6ab569 --- /dev/null +++ b/src/swap/superdata/SuperBin.java @@ -0,0 +1,43 @@ +package swap.superdata; + +import java.io.File; + +import swap.data.DataFile; +import swap.data.DataObject; +import swap.data.DataString; + +public class SuperBin { + public final SuperSet set; + public final int number; + public final boolean local; + + SuperBin(SuperSet set, int number, boolean local) { + this.set = set; + this.number = number; + this.local = local; + } + + SuperBin(SuperSet set, int number, DataObject.Map object) { + this.set = set; + this.number = number; + if (object.count() != 1) { + throw new Error("Invalid bin data"); + } + DataObject v = object.get(DataObject.of("local")); + this.local = v.equals(DataObject.TRUE); + } + + public DataObject.Map toDataObject() { + DataObject.Map m = new DataObject.Map(); + m.set(DataObject.of("local"), DataObject.of(local)); + return m; + } + + public DataFile openIndex() { + return set.getLock().open(new File(set.binIndexPath(number))); + } + + public DataString[] listAll() { + return openIndex().getKeys(); + } +} diff --git a/src/swap/superdata/SuperDoc.java b/src/swap/superdata/SuperDoc.java new file mode 100644 index 0000000..a4f8bdd --- /dev/null +++ b/src/swap/superdata/SuperDoc.java @@ -0,0 +1,68 @@ +package swap.superdata; + +import swap.data.DataObject; +import swap.data.DataString; +import swap.data.DataObject.Map; + +/** + * A SuperDoc is an ordinary object which holds document state. Generally, a SuperDoc is created + * by a SuperSet object (either representing a new or existing document) and then it's contents + * (but not the other fields) are modified as necessary by the user. The version string is updated + * automatically if you store the document. + * + * @author Zak + * + */ +public class SuperDoc { + public DataString title; + public DataString version; + public DataString contents; + + SuperDoc(DataString title, DataString version, DataString contents) { + this.title = title; + this.version = version; + this.contents = contents; + } + + public boolean isNew() { + return version == null; + } + + public boolean isDeleted() { + return contents == null && version != null; + } + + public int getVersionSequence() { + if (version == null) { + return 0; + } + DataString start = version.sub(0, 4); // First 4 digits are (decimal) sequence number, mostly for sorting purposes + int x = Integer.parseInt(start.getUTF8()); + return x; + } + + public static String sequenceString(int n) { + n = n % 10000; // Keep it to 4 digits + String r = "" + n; + while (r.length() < 4) { + r = "0" + r; + } + return r; + } + + public String nextVersionString(DataObject credentials) { + int nextSequence = getVersionSequence() + 1; + DataObject.Map m = new DataObject.Map(); + m.set("title", DataObject.of(title)); + m.set("version", version == null ? null : DataObject.of(version)); + m.set("meta", null); + m.set("credentials", credentials); + m.set("contents", contents == null ? null : DataObject.of(contents)); + int hash = DataObject.encodeSimple(m).hashCode(); + String hashstr = Integer.toHexString(hash); + while (hashstr.length() < 8) { + hashstr = "0" + hashstr; + } + return sequenceString(nextSequence) + "-" + hashstr; + } +} diff --git a/src/swap/superdata/SuperLock.java b/src/swap/superdata/SuperLock.java new file mode 100644 index 0000000..3051128 --- /dev/null +++ b/src/swap/superdata/SuperLock.java @@ -0,0 +1,46 @@ +package swap.superdata; + +import java.io.File; +import java.util.WeakHashMap; + +import swap.data.DataFile; + +/** + * This is only used to ensure that exactly/at-most one DataFile object exists within a + * single VM at a single time. This is only necessary for applications which will randomly open + * databases from multiple threads, in which case multiple SuperLock instances can be used + * if only certain databases are used by certain threads (otherwise, the default instance can + * be used). The SuperSet class will use the default SuperLock if none is passed to it, meaning + * you can create/use multiple SuperSet instances within a single VM (because the databases accessed + * within it will all be locked/shared properly). + * + *

All of this is only required because the same file can't be locked from two threads of + * the same application (at least on some systems/JVMs if not more generally). Between two different + * application instances running at the same time on the same system, the internal file locking should + * be sufficient to synchronise database access (that synchronisation system is half the reason for using + * such a database - to ensure data integrity with multiple simultaneous readers/writers). + * + * @author Zak + * + */ +public class SuperLock { + private static final SuperLock instance = new SuperLock(); + + public static SuperLock getDefaultInstance() { + return instance; + } + + private WeakHashMap databases = new WeakHashMap(); + + public synchronized DataFile open(File f) { + f = f.getAbsoluteFile(); + String p = f.getAbsolutePath(); + if (databases.containsKey(p)) { + return databases.get(p); + } else { + DataFile d = new DataFile(f); + databases.put(p, d); + return d; + } + } +} diff --git a/src/swap/superdata/SuperSet.java b/src/swap/superdata/SuperSet.java new file mode 100644 index 0000000..14d7695 --- /dev/null +++ b/src/swap/superdata/SuperSet.java @@ -0,0 +1,312 @@ +package swap.superdata; + +import java.io.File; + +import swap.data.DataFile; +import swap.data.DataObject; +import swap.data.DataObject.Text; +import swap.data.DataOp; +import swap.data.DataString; +import swap.data.DataTransaction; +import swap.data.DataVariable; + +/** + * Essentially, this class creates a larger key-value store using a set of smaller key-value stores. This + * allows somewhat-consistent read/write access from many threads without each thread having to (always) + * lock the same central database file. Individual key/value pairs are combined with metadata and stored + * together in bins based on a hash of the key, meaning that if you have 10 bins in a superset you only + * have to lock 1 bin to interact with one key/value pair - and if a second thread is accessing another key + * there's only a 1 in 10 chance it will be in the same bin. + * + *

This approach guarantees (as much as possible) the integrity of each key/value pair even if accessed + * from many processes and ensures the database as a whole isn't corrupted by smaller failures (such as a single + * file being corrupted). + * + *

The downside of this approach is that it's harder to lock the whole database in order to interact + * with multiple key/value pairs in synchronicity. This means that a SuperSet operates like a document + * database, i.e. each key/value pair represents a document, and synchronisation is generally at the level + * of "a version of a document" (similar to CouchDB for instance). This means that applications or other layers + * may need to do have additional synchronisation logic (as is generally the case when dealing with gigantic + * datasets). + * + * @author Zak + * + */ +public class SuperSet { + private final String pathBase; + private final SuperLock lock; + private DataFile configDB; + //private int bins; + private SuperBin[] bins; + + public SuperSet(File path) { + this(SuperLock.getDefaultInstance(), path); + } + + public SuperSet(SuperLock lock, File path) { + this(lock, path, 32, true); + } + + public SuperSet(SuperLock lock, File path, int defaultBins, boolean defaultLocal) { + this.lock = lock; + path = path.getAbsoluteFile(); + if (!path.exists()) { + path.mkdirs(); + } + if (!path.isDirectory()) { + throw new Error("SuperSet path '" + path + "' is not a directory!"); + } + this.pathBase = path.getAbsolutePath(); + configDB = lock.open(new File(path.getPath() + "/config.db")); + + configDB.doOp(new DataOp() { + + @Override + public void perform(DataTransaction x) { + boolean setup = false; + String v = x.getUTF8("bins"); + int nbins; + if (v == null) { + nbins = defaultBins; + setup = true; + } else { + nbins = Integer.parseInt(v); + } + if (nbins < 1 || nbins > 1000000000) { + throw new Error("Invalid number of bins: " + nbins); + } + bins = new SuperBin[nbins]; + + /* Each bin has a record in the config database, in particular to indicate whether it's + * kept locally (future versions/extensions may distribute supersets between nodes, + * e.g. every second bin on node A and the other half on node B, except likely more nodes!). + */ + for (int i = 0; i < nbins; i++) { + String n = "bin" + binName(i); + if (setup) { + bins[i] = new SuperBin(SuperSet.this, i, defaultLocal); + x.setValue(n, bins[i].toDataObject()); + new File(binDataPath(i)).mkdirs(); + } else { + DataObject o = x.getDataObject(n); + if (!(o instanceof DataObject.Map)) { + throw new Error("Expected map describing bin, got: " + o); + } + bins[i] = new SuperBin(SuperSet.this, i, (DataObject.Map)o); + } + } + + x.finish(); + } + }); + } + + /** + * Returns the bin number in a canonical text format (with leading 0s if necessary to match the longest number + * in this superset). + * @param binNumber + * @return + */ + public String binName(int binNumber) { + String result = Integer.toHexString(binNumber); + String longest = Integer.toHexString(bins.length-1); + while (result.length() < longest.length()) { + result = "0" + result; + } + return result.toUpperCase(); + } + + public String binDataPath(int binNumber) { + return pathBase + "/data" + binName(binNumber); + } + + public String binIndexPath(int binNumber) { + return pathBase + "/index" + binName(binNumber) + ".db"; + } + + /** + * Returns the bin with the given number (assumed to be either the bin id, or a hash - which, modulo the number of bins, will be reduced to the bin id). + * @param binHash + * @return + */ + public SuperBin bin(int binHash) { + binHash &= 0x7FFFFFFF; // Ignore sign bit. + binHash %= bins.length; + return bins[binHash]; + } + + public SuperBin bin(DataString s) { + return bin(s.hashCode()); + } + + public SuperBin bin(String s) { + return bin(DataString.ofUTF8(s)); + } + + public SuperLock getLock() { + return lock; + } + + /** + * Fetches a list of every document in every bin. This is not synchronised between bins (an earlier bin + * may have documents added while this function is fetching contents of a later bin in parallel) and it + * does not take into account deleted objects (these will still be returned if any version/record of them + * exists). + * + * @return A list of all document titles in the superset. + */ + public DataString[] listAll() { + DataString[][] l = new DataString[bins.length][]; + int len = 0; + for (int i = 0; i < bins.length; i++) { + l[i] = bins[i].listAll(); + len += l[i].length; + } + DataString[] result = new DataString[len]; + int t = 0; + for (int i = 0; i < l.length; i++) { + for (int j = 0; j < l[i].length; j++) { + result[t] = l[i][j]; + t++; + } + } + return result; + } + + private SuperVersion listOneVersion(DataObject.Map map, DataObject.Text version) { + DataObject o = map.get(version); + if (o instanceof DataObject.Text) { + return new SuperVersion(version.value, false); + } else { + return new SuperVersion(version.value, true); + } + } + + /** + * Lists basic version information about a document, starting with the latest version (the rest in random order). + * @param title + * @return + */ + public SuperVersion[] listVersions(DataString title) { + SuperBin bin = bin(title); + DataFile db = bin.openIndex(); + DataObject stored = db.getDataObject(title); + if (stored == null) { + return new SuperVersion[0]; + } + if (!(stored instanceof DataObject.Map)) { + throw new Error("Data in wrong format"); + } + DataObject.Map m = (DataObject.Map) stored; + if (!m.has("latest")) { + throw new Error("Data in wrong format"); + } + DataObject tmp = m.get("latest"); + if (!(tmp instanceof DataObject.Text)) { + throw new Error("Data in wrong format"); + } + Text[] keys = m.keys(); + SuperVersion[] result = new SuperVersion[keys.length - 1]; + result[0] = listOneVersion(m, (DataObject.Text) tmp); + int idx = 1; + for (Text t: keys) { + if (!(t.equals(DataObject.of("latest")) || t.equals(tmp))) { + result[idx] = listOneVersion(m, t); + idx++; + } + } + return result; + } + + public SuperDoc request(DataString title, DataString version, DataObject credentials) { + SuperBin bin = bin(title); + DataFile db = bin.openIndex(); + DataObject stored = db.getDataObject(title); + if (stored == null) { + if (version != null) { + throw new Error("Version doesn't exist"); + } + return new SuperDoc(title, null, null); + } + if (!(stored instanceof DataObject.Map)) { + throw new Error("Data in wrong format"); + } + DataObject.Map m = (DataObject.Map) stored; + if (version == null) { + if (!m.has("latest")) { + throw new Error("Data in wrong format"); + } + DataObject tmp = m.get("latest"); + if (!(tmp instanceof DataObject.Text)) { + throw new Error("Data in wrong format"); + } + version = ((DataObject.Text) tmp).value; + } + DataObject entry = m.get(DataObject.of(version)); + if (entry == null) { + throw new Error("Version doesn't exist"); + } + /* Simplest format - data is embedded as a Text. */ + if (entry instanceof DataObject.Text) { + DataString data = ((DataObject.Text) entry).value; + return new SuperDoc(title, version, data); + } + /* TODO: Individual storage for large documents? + * if (entry instanceof DataObject.Map) { + + }*/ + throw new Error("Data in wrong format"); + } + + public boolean submit(final SuperDoc document, DataObject credentials) { + final SuperBin bin = bin(document.title); + final DataString newver = DataString.ofUTF8(document.nextVersionString(credentials)); + final DataVariable isLatest = new DataVariable(false); + bin.openIndex().doOp(new DataOp() { + @Override + public void perform(DataTransaction x) { + DataObject stored = x.getDataObject(document.title); + DataObject.Map m; + boolean waslatest = false; + if (stored == null) { + m = new DataObject.Map(); + waslatest = true; + } else { + if (!(stored instanceof DataObject.Map)) { + throw new Error("Data in wrong format"); + } + m = (DataObject.Map)stored; + + /* If the latest stored document matches the (not-yet-updated) version in the document object, then + * the new version will be the latest version stored here. + */ + DataObject l = m.get("latest"); + if (!(l instanceof DataObject.Text)) { + throw new Error("Data in wrong format"); + } + DataString storedver = ((DataObject.Text) l).value; + if (storedver.equals(document.version)) { + waslatest = true; + } + } + + if (m.has(DataObject.of(newver))) { + throw new Error("Version conflict"); + } + + m.set(DataObject.of(newver), document.contents == null ? null : DataObject.of(document.contents)); + + if (waslatest) { + m.set("latest", DataObject.of(newver)); + isLatest.setValue(true); + } + x.setValue(document.title, m); + x.finish(); + } + }); + + document.version = newver; + + return isLatest.getValue(); + } +} diff --git a/src/swap/superdata/SuperVersion.java b/src/swap/superdata/SuperVersion.java new file mode 100644 index 0000000..d545518 --- /dev/null +++ b/src/swap/superdata/SuperVersion.java @@ -0,0 +1,20 @@ +package swap.superdata; + +import swap.data.DataString; + +public class SuperVersion { + public DataString version; + public boolean deleted; + + public SuperVersion(DataString version, boolean deleted) { + this.version = version; + this.deleted = deleted; + } + + @Override + public String toString() { + return "SuperVersion [version=" + version.getUTF8() + ", deleted=" + deleted + "]"; + } + + +} diff --git a/src/swap/template/Attribute.java b/src/swap/template/Attribute.java new file mode 100644 index 0000000..7440dd3 --- /dev/null +++ b/src/swap/template/Attribute.java @@ -0,0 +1,19 @@ +package swap.template; + +public class Attribute { + private final String key; + private final String value; + + public Attribute(String key, String value) { + super(); + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + public String getValue() { + return value; + } +} diff --git a/src/swap/template/Element.java b/src/swap/template/Element.java new file mode 100644 index 0000000..bc98963 --- /dev/null +++ b/src/swap/template/Element.java @@ -0,0 +1,116 @@ +package swap.template; + +import java.io.PrintStream; +import java.util.ArrayList; + +public class Element extends Node { + private final String tag; + private final ArrayList nodes = new ArrayList(); + + private final ArrayList headers = new ArrayList(); + + public Element(String tag) { + this.tag = tag; + } + + public String getTag() { + return tag; + } + + public void addAttribute(Attribute attribute) { + headers.add(attribute); + } + + public void addAttribute(String key, String value) { + addAttribute(new Attribute(key, value)); + } + + public int countAttributes() { + return headers.size(); + } + + public Attribute getAttribute(int i) { + if (i < 0 || i >= headers.size()) { + return null; + } else { + return headers.get(i); + } + } + + public Attribute getAttribute(String key) { + for (int i = headers.size() - 1; i >= 0; i--) { + if (headers.get(i).getKey().equals(key)) { + return headers.get(i); + } + } + return null; + } + + public boolean hasAttribute(String key) { + return getAttribute(key) != null; + } + + public void addNode(Node node) { + nodes.add(node); + } + + public void addAttribute(String text) { + addNode(new Text(text)); + } + + public int countNodes() { + return nodes.size(); + } + + public Node getNode(int i) { + if (i < 0 || i >= nodes.size()) { + return null; + } else { + return nodes.get(i); + } + } + + @Override + public void printRecursively(PrintStream target, String indent, String nextIndent) { + target.print(indent + "<" + getTag()); + for (int i = 0; i < countAttributes(); i++) { + Attribute a = getAttribute(i); + if (i != 0) { + target.print(" "); + } + target.print(a.getKey() + "=\"" + a.getValue() + "\""); // TODO: Escape it properly + } + + if (countNodes() < 1) { + target.println("/>"); + return; + } + + target.println(">"); + + for (int i = 0; i < countNodes(); i++) { + Node n = getNode(i); + n.printRecursively(target, indent + nextIndent, nextIndent); + } + + target.println(indent + ""); + } + + public Element a(String key, String value) { + return a(new Attribute(key, value)); + } + + public Element a(Attribute attribute) { + addAttribute(attribute); + return this; + } + + public Element n(String title) { + return n(new Text(title)); + } + + public Element n(Node node) { + addNode(node); + return this; + } +} diff --git a/src/swap/template/Node.java b/src/swap/template/Node.java new file mode 100644 index 0000000..7b84e9a --- /dev/null +++ b/src/swap/template/Node.java @@ -0,0 +1,39 @@ +package swap.template; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; + +import swap.httpd.Content; + +public abstract class Node implements Content { + @Override + public String getType() { + return "text/html"; + } + + @Override + public byte[] getPayload() { + ByteArrayOutputStream o = new ByteArrayOutputStream(); + PrintStream p; + try { + p = new PrintStream(o, true, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new Error("Encoding problem", e); + } + printRecursively(p, "", " "); + p.flush(); + return o.toByteArray(); + } + + public abstract void printRecursively(PrintStream target, String indent, String nextIndent); + + @Override + public String toString() { + try { + return new String(getPayload(), "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new Error("Encoding problem", e); + } + } +} diff --git a/src/swap/template/Page.java b/src/swap/template/Page.java new file mode 100644 index 0000000..a596af8 --- /dev/null +++ b/src/swap/template/Page.java @@ -0,0 +1,63 @@ +package swap.template; + +import java.io.UnsupportedEncodingException; + +import swap.httpd.Content; + +/** + * A simple high-level page template. + * + * @author Zak + * + */ +public class Page implements Content { + private final Element html; + private final Element head; + private final Element body; + + public Page(String title) { + html = new Element("html"); + head = new Element("head"); + html.addNode(head); + body = new Element("body"); + html.addNode(body); + head.addNode(new Element("title").n(title)); + head.addNode(new Element("meta").a("charset", "utf-8")); + } + + @Override + public String getType() { + return "text/html"; + } + + @Override + public byte[] getPayload() { + try { + return ("\n" + html).getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new Error("Encoding problem", e); + } + } + + public Page h(Node node) { + head.addNode(node); + return this; + } + + public Page b(Node node) { + body.addNode(node); + return this; + } + + public Element getHTML() { + return html; + } + + public Element getHead() { + return head; + } + + public Element getBody() { + return body; + } +} diff --git a/src/swap/template/StructuredPage.java b/src/swap/template/StructuredPage.java new file mode 100644 index 0000000..1796498 --- /dev/null +++ b/src/swap/template/StructuredPage.java @@ -0,0 +1,10 @@ +package swap.template; + +public class StructuredPage extends Page { + + public StructuredPage(String title) { + super(title); + // TODO Auto-generated constructor stub + } + +} diff --git a/src/swap/template/Text.java b/src/swap/template/Text.java new file mode 100644 index 0000000..94a2b04 --- /dev/null +++ b/src/swap/template/Text.java @@ -0,0 +1,78 @@ +package swap.template; + +import java.io.PrintStream; + +public class Text extends Node { + private final String raw; + + public Text(String text) { + raw = text; + } + + public String getRaw() { + return raw; + } + + @Override + public void printRecursively(PrintStream target, String indent, String nextIndent) { + target.println(indent + rawhtml(raw)); + } + + static boolean isGarbage(char c) { + switch (c) { + case ':': + case ';': + case '.': + case '(': + case ')': + case '|': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + return true; + } + return !Character.isAlphabetic(c); + } + + public static String trim(String s) { + s = s.toLowerCase(); + while (s.length() >= 1) { + if (isGarbage(s.charAt(0))) { + s = s.substring(1); + } else if (isGarbage(s.charAt(s.length() - 1))) { + s = s.substring(0, s.length() - 1); + } else { + return s; + } + } + return s; + } + + public static boolean isEnglish(char x) { + return (x >= 'a' && x <= 'z') || (x >= 'A' && x <= 'Z') || (x >= '0' && x <= '9'); + } + + public static String rawhtml(String text) { + int[] u = text.codePoints().toArray(); + String r = ""; + for (int x: u) { + if (x == ' ') { + r += " "; + } else if (x == '\n') { + r += "\n

\n"; + } else if (((int)(char)x) != x || isGarbage((char)x) || !isEnglish((char)x)) { + r += "&#" + x + ";"; + } else { + r += new String(Character.toChars(x)); + } + } + return r; + } +} diff --git a/src/swap/testing/DBTest.java b/src/swap/testing/DBTest.java new file mode 100644 index 0000000..176677d --- /dev/null +++ b/src/swap/testing/DBTest.java @@ -0,0 +1,74 @@ +package swap.testing; + +import java.io.File; + +import swap.data.DataFile; +import swap.data.DataOp; +import swap.data.DataString; +import swap.data.DataTransaction; + +public class DBTest { + + public static void innerTest(DataFile f) { + DataString prev = f.getValue(DataString.ofUTF8("foo")); + String str = ""; + DataString prevBar = f.getValue(DataString.ofUTF8("bar")); + String strBar = ""; + if (prev != null) { + str = prev.getUTF8(); + } + if (prevBar != null) { + strBar = prevBar.getUTF8(); + } + System.out.println("foo='" + str + "'"); + System.out.println("bar='" + strBar + "'"); + + //f.setValue(DataString.ofUTF8("foo"), DataString.ofUTF8(str + "bar")); + f.doOp(new DataOp() { + + @Override + public void perform(DataTransaction x) { + DataString dataX = x.getValue(DataString.ofUTF8("foo")); + String strX = ""; + if (dataX != null) { + strX = dataX.getUTF8(); + } + x.setValue(DataString.ofUTF8("foo"), DataString.ofUTF8(strX + "bar")); + x.setValue(DataString.ofUTF8("bar"), DataString.ofUTF8(strX)); + x.setValue(DataString.ofUTF8("baz"), DataString.ofUTF8(strX)); + x.setValue(DataString.ofUTF8("bong"), DataString.ofUTF8(strX)); + x.setValue(DataString.ofUTF8("bus"), DataString.ofUTF8(strX)); + x.setValue(DataString.ofUTF8(strX), DataString.ofUTF8(strX)); + x.finish(); + } + }); + prev = f.getValue(DataString.ofUTF8("foo")); + str = ""; + if (prev != null) { + str = prev.getUTF8(); + } + prevBar = f.getValue(DataString.ofUTF8("bar")); + strBar = ""; + if (prevBar != null) { + strBar = prevBar.getUTF8(); + } + System.out.println("foo='" + str + "'"); + System.out.println("bar='" + strBar + "'"); + } + + public static void main(String[] args) { + String dtp = System.getProperty("user.home") + "/OneDrive/Desktop/TestDir"; + final DataFile f = new DataFile(new File(dtp + "/DBTest.db")); + for (int i = 0; i < 20; i++) { + Thread t = new Thread(new Runnable() { + + @Override + public void run() { + innerTest(f); + } + }); + t.start(); + } + } + +} diff --git a/src/swap/testing/SuperTest.java b/src/swap/testing/SuperTest.java new file mode 100644 index 0000000..a367adc --- /dev/null +++ b/src/swap/testing/SuperTest.java @@ -0,0 +1,39 @@ +package swap.testing; + +import java.io.File; + +import swap.data.DataString; +import swap.superdata.SuperDoc; +import swap.superdata.SuperSet; +import swap.superdata.SuperVersion; + +public class SuperTest { + + public static void main(String[] args) { + String dtp = System.getProperty("user.home") + "/OneDrive/Desktop/TestDir"; + String superdb = dtp + "/supdb"; + + SuperSet set = new SuperSet(new File(superdb)); + + SuperDoc a = set.request(DataString.ofUTF8("a"), null, null); + if (a.isNew()) { + a.contents = DataString.ofUTF8("START"); + } else { + System.out.println("Found old data: '" + a.contents.getUTF8() + "'"); + a.contents = a.contents.cat(DataString.ofUTF8("a")); + SuperDoc another = set.request(a.contents, null, null); + another.contents = DataString.ofUTF8("This is just added as a test"); + set.submit(another, null); + } + set.submit(a, null); + + DataString[] docs = set.listAll(); + for (DataString d: docs) { + System.out.println("Has document '" + d.getUTF8() + "'"); + for (SuperVersion v: set.listVersions(d)) { + System.out.println(" > " + v); + } + } + } + +} diff --git a/src/swap/util/JSON.java b/src/swap/util/JSON.java new file mode 100644 index 0000000..d689a58 --- /dev/null +++ b/src/swap/util/JSON.java @@ -0,0 +1,2279 @@ +//This is free and unencumbered software released into the public domain. +// +//Anyone is free to copy, modify, publish, use, compile, sell, or +//distribute this software, either in source code form or as a compiled +//binary, for any purpose, commercial or non-commercial, and by any +//means. +// +//In jurisdictions that recognize copyright laws, the author or authors +//of this software dedicate any and all copyright interest in the +//software to the public domain. We make this dedication for the benefit +//of the public at large and to the detriment of our heirs and +//successors. We intend this dedication to be an overt act of +//relinquishment in perpetuity of all present and future rights to this +//software under copyright law. +// +//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +//EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +//MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +//IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +//OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +//ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +//OTHER DEALINGS IN THE SOFTWARE. +// +//For more information, please refer to +package swap.util; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; +import java.util.Objects; + +/** + * This is a simple, lightweight JSON parsing and writing API. It parses JSON + * files according to + * https://www.json.org/json-en.html + * and converts the values to Java types. Furthermore it accepts Java types and + * stores them such that they themselves can be written to a file or stream. + *

+ * As by JSON specification (key-value) Objects as well as Strings, Numbers, + * "true", "false", null, and arrays are all valid JSON and can be parsed with + * this implementation. + *
The implementation follows, when parsing, the context-free grammar of + * JSON which means that duplicate keys will not throw and error and the last + * parsed value for any given duplicate key will be used. When writing, no + * duplicate keys will be produced, since the internal Java HashMap will only + * hold the latest value for any given key. + *

+ * Every JSON instance can be validated by a subset of constraints of the + * https://json-schema.org/ + * specification. JSON schema is a JSON validation scheme defined itself in JSON + *

+ * What is special in this implementation? + *
+ *

    + *
  • + * More white space characters are allowed: + *
    JSON specification allows "", u0020, u000A, u000D, u0009 + *
    This implementation allows every Character defined by + * {@link java.lang.Character#isWhitespace} + *
  • + *
  • + * Types are converted to Java number types: + *
    JSON specification allows arbitrary length integers and arbitrary + * precision floating point values. + *
    This implementation can read Java signed Integers and automatically + * convert them to a Java signed Long, when Integer.MAX_VALUE is exceeded.No + * automatic conversion to BigInteger. All read floating point values will + * be converted to a Java double. + *
  • + *
  • + * Upon insertion of a Java float, this implementation will automatically + * convert it to a double. + *
  • + *
+ */ +public final class JSON implements Iterable { + + private final static char EOF = (char) -1; + + /** + * Whether or not forward slashes will be escaped in any JSON String + * written. (Default: false). The JSON specification allows the escape of + * forward slashes "/" as "\/" but it is optional. + */ + public static boolean ESCAPE_FORWARD_SLASHES = false; + + /** + * The type a JSON instance can have. One of: + *
    + *
  • JSON_OBJECT
  • + *
  • JSON_ARRAY
  • + *
  • JSON_STRING
  • + *
  • JSON_NUMBER
  • + *
  • TRUE
  • + *
  • FALSE
  • + *
  • NULL
  • + *
+ */ + public static enum JSONVALUE_TYPE { + JSON_OBJECT, + JSON_ARRAY, + JSON_STRING, + JSON_NUMBER, + TRUE, + FALSE, + NULL + } + + private static enum LITERAL { + TRUE(JSONVALUE_TYPE.TRUE, "true", true), + FALSE(JSONVALUE_TYPE.FALSE, "false", false), + NULL(JSONVALUE_TYPE.NULL, "null", null); + + public final JSONVALUE_TYPE type; + public final String name; + public final Object value; + + private LITERAL(JSONVALUE_TYPE _type, String _literal, Object _literalValue) { + type = _type; + name = _literal; + value = _literalValue; + } + + private static LITERAL FROM_TYPE(JSONVALUE_TYPE _type) { + switch (_type) { + case FALSE: + return (FALSE); + case TRUE: + return (TRUE); + case NULL: + return (NULL); + default: + throw new IllegalArgumentException("Not a valid literal"); + } + } + } + + private final static class JSONVALUE { + + private static String JAVASTRING2JSONSTRING(final String _string) { + String ret = _string.replace("\\", "\\\\"); + if (ESCAPE_FORWARD_SLASHES) { + ret = ret.replace("/", "\\/"); + } + ret = ret.replace("\"", "\\\""); + ret = ret.replace("\b", "\\b"); + ret = ret.replace("\f", "\\f"); + ret = ret.replace("\n", "\\n"); + ret = ret.replace("\r", "\\r"); + ret = ret.replace("\t", "\\t"); + for (char c = 0x0; c < 0x20; c++) { + if (ret.indexOf(c) != -1) { + String hex = ("0000" + Integer.toHexString(c)); + ret = ret.replace(Character.toString(c), "\\u" + hex.substring(hex.length() - 4, hex.length())); + } + } + return (ret); + } + + private final JSONVALUE_TYPE type; + private final Object value; + + private JSONVALUE(final JSONVALUE_TYPE _type, final Object _object) { + type = _type; + value = _object; + } + + private JSONVALUE(final LITERAL _literal) { + type = _literal.type; + value = _literal.value; + } + + private JSONVALUE(final Object _value) { + if (_value == null) { + type = LITERAL.NULL.type; + value = LITERAL.NULL.value; + } else if (_value instanceof Boolean) { + type = (boolean) _value ? LITERAL.TRUE.type : LITERAL.FALSE.type; + value = (boolean) _value ? LITERAL.TRUE.value : LITERAL.FALSE.value; + } else if (_value instanceof String) { + type = JSONVALUE_TYPE.JSON_STRING; + value = _value; + } else if (_value instanceof Float) { + type = JSONVALUE_TYPE.JSON_NUMBER; + value = (double) (float) _value; + } else if (_value instanceof Number) { + type = JSONVALUE_TYPE.JSON_NUMBER; + value = _value; + } else if (_value instanceof JSON) { + //Copy reference if _value.root is OBJECT or ARRAY + type = ((JSON) _value).root_.type; + value = ((JSON) _value).root_.value; + } else { + throw new IllegalArgumentException("Not a serializable value (" + _value.getClass().getName() + ")"); + } + } + + private Object toValue() { + switch (type) { + case JSON_ARRAY: + case JSON_OBJECT: + return (new JSON(this)); + default: + return (value); + } + } + + @Override + public String toString() { + switch (type) { + case TRUE: + case FALSE: + case NULL: + case JSON_NUMBER: + case JSON_STRING: + return (String.valueOf(value) + " (" + type.name() + ")"); + case JSON_ARRAY: + return (type.name() + " " + ((ArrayList) value).size() + " entries"); + case JSON_OBJECT: + return (type.name() + " " + ((HashMap) value).size() + " entries"); + } + + throw new IllegalStateException("Invalid JSONVALUE"); + } + + @Override + public boolean equals(final Object _other) { + if (_other == null || !(_other instanceof JSONVALUE)) { + return (false); + } + + JSONVALUE other = (JSONVALUE) _other; + + if (type != other.type) { + return (false); + } + + switch (type) { + case NULL: + case TRUE: + case FALSE: + case JSON_NUMBER: + return (value == other.value); + case JSON_STRING: + return (value.equals(other.value)); + case JSON_ARRAY: + return (((ArrayList) value).equals(other.value)); + case JSON_OBJECT: + return (((HashMap) value).equals(other.value)); + } + + return (false); + } + + private boolean equalsValue(final Object _other) { + switch (type) { + case NULL: + case TRUE: + case FALSE: + case JSON_NUMBER: + if (_other instanceof Float) { + return ((double) value == (double) (float) _other); + } + return (value == _other); + case JSON_STRING: + return (value.equals(_other)); + case JSON_ARRAY: + return (toValue().equals(_other)); + case JSON_OBJECT: + return (toValue().equals(_other)); + } + return (false); + } + + @Override + public int hashCode() { + int hash = 5; + hash = 61 * hash + Objects.hashCode(type); + hash = 61 * hash + Objects.hashCode(value); + return hash; + } + + private JSONVALUE copy() { + switch (type) { + case NULL: + return (new JSONVALUE(LITERAL.NULL)); + case TRUE: + return (new JSONVALUE(LITERAL.TRUE)); + case FALSE: + return (new JSONVALUE(LITERAL.FALSE)); + case JSON_STRING: + return (new JSONVALUE(JSONVALUE_TYPE.JSON_STRING, ((String) value))); + case JSON_NUMBER: + return (new JSONVALUE(JSONVALUE_TYPE.JSON_NUMBER, value)); + case JSON_ARRAY: + ArrayList newarray = new ArrayList<>(); + for (JSONVALUE oldvalue : (ArrayList) value) { + newarray.add(oldvalue.copy()); + } + return (new JSONVALUE(JSONVALUE_TYPE.JSON_ARRAY, newarray)); + case JSON_OBJECT: + HashMap newobject = new HashMap<>(); + for (Map.Entry oldentry : ((HashMap) value).entrySet()) { + newobject.put(oldentry.getKey(), oldentry.getValue().copy()); + } + return (new JSONVALUE(JSONVALUE_TYPE.JSON_OBJECT, newobject)); + } + throw new IllegalStateException("Invalid JSONVALUE"); + } + + private String json_toString(final boolean _whitespace, final int _depth) { + switch (type) { + case NULL: + case FALSE: + case TRUE: + case JSON_NUMBER: + return (String.valueOf(value)); + case JSON_STRING: + return ("\"" + JAVASTRING2JSONSTRING((String) value) + "\""); + case JSON_ARRAY: { + String ret = "["; + boolean first = true; + for (JSONVALUE v : (ArrayList) value) { + if (!first) { + ret += ","; + if (_whitespace) { + ret += " "; + } + } + ret += v.json_toString(_whitespace, _depth); + first = false; + } + ret += "]"; + return (ret); + } + case JSON_OBJECT: { + String ret = "{"; + boolean first = true; + for (Map.Entry entry : ((HashMap) value).entrySet()) { + if (!first) { + ret += ","; + } + if (_whitespace) { + ret += "\n"; + } + if (_whitespace) { + for (int i = 0; i < _depth; i++) { + ret += "\t"; + } + } + ret += "\"" + entry.getKey() + "\":"; + if (_whitespace) { + ret += " "; + } + if (entry.getValue().type == JSONVALUE_TYPE.JSON_OBJECT) { + ret += entry.getValue().json_toString(_whitespace, _depth + 1); + } else { + ret += entry.getValue().json_toString(_whitespace, _depth); + } + first = false; + } + if (_whitespace) { + ret += "\n"; + } + if (_whitespace) { + for (int i = 0; i < _depth - 1; i++) { + ret += "\t"; + } + } + ret += "}"; + return (ret); + } + } + + return (""); + } + } + + private static class RETURN_TUPLE { + + private final JSONVALUE returnValue; + private final char returnChar; + + public RETURN_TUPLE(JSONVALUE _returnValue, char _returnChar) { + returnValue = _returnValue; + returnChar = _returnChar; + } + + } + + @FunctionalInterface + private static interface CHAR_SUPPLIER { + + char nextChar(); + } + + private JSON(final JSONVALUE _value) { + root_ = _value; + } + + /** + * Returns a JSON representation of a string by itself. + * (Added by Zak) + * + * @param value + * @return + */ + public static JSON rawString(String value) { + return new JSON(new JSONVALUE(value)); + } + + /** + * Added by Zak - how else to get internal value of lone string etc.?? + * @return + */ + public Object rawValue() { + if (root_ == null) { + return null; + } else if (root_ instanceof JSONVALUE) { + return ((JSONVALUE)root_).toValue(); + } else { + return root_; + } + } + + //========================================================================== + //# # + //# Provide different constructors to cover different sources of JSON. # + //# Each constructor calls the respective init method. The init methods # + //# clear the current JSON and parse the given source. JSON class is # + //# therefor mutable. # + //# # + //========================================================================== + private JSONVALUE root_; + + /** + * Creates a new empty JSON. The type of this will be + * {@link JSONVALUE_TYPE#NULL}. + *
+ * However with the methods + *
{@link JSON#json_add(java.lang.Object)}, + *
{@link JSON#json_add(java.lang.String, java.lang.Object)}, + *
{@link JSON#json_add(int, java.lang.Object)} + *
this JSON will be automatically converted to an Key-Value object or + * array. + */ + public JSON() { + json_init(); + } + + /** + * Creates a new "empty" JSON, but sets the type according to the specified + * parameter. + *
    + *
  • If the _type is a LITERAL, the literal value is used.
  • + *
  • If the _type is a {@link JSONVALUE_TYPE#JSON_NUMBER}, the value 0 is + * used.
  • + *
  • If the _type is a {@link JSONVALUE_TYPE#JSON_STRING}, the empty + * String ("") is used.
  • + *
  • If the _type is an {@link JSONVALUE_TYPE#JSON_ARRAY}, a new empty + * array will be created.
  • + *
  • If the _type is an {@link JSONVALUE_TYPE#JSON_OBJECT}, a new empty + * object will be created.
  • + *
+ * + * @param _type The type of this JSON + */ + public JSON(JSONVALUE_TYPE _type) { + json_init(_type); + } + + /** + * Creates a new JSON and deep copies all values of the specified JSON. (No + * references will be reused) + * + * @param _other The JSON to be copied + */ + public JSON(final JSON _other) { + json_init(_other); + } + + /** + * Creates a new JSON and parses the specified file + * + * @param _file The file to be parsed + * @throws IOException If file reading fails + * @throws IllegalStateException If the parsing fails due to a syntax error + */ + public JSON(final File _file) throws IOException, IllegalStateException { + json_init(_file); + } + + /** + * Creates a new JSON and parses the specified input stream + * + * @param _input The stream to be parsed + * @throws IOException If file reading fails + * @throws IllegalStateException If the parsing fails due to a syntax error + */ + public JSON(final InputStream _input) throws IOException, IllegalStateException { + json_init(_input); + } + + /** + * Creates a new JSON and parses the specified String + * + * @param _input The String to be parsed + * @throws IllegalStateException If the parsing fails due to a syntax error + */ + public JSON(final String _input) throws IllegalStateException { + json_init(_input); + } + + /** + * Creates a new JSON and parses the specified input + * + * @param _input The bytes to be parsed + * @param _encoding The encoding of the specified bytes + * @throws IllegalStateException If the parsing fails due to a syntax error + */ + public JSON(final byte[] _input, final Charset _encoding) throws IllegalStateException { + json_init(_input, _encoding); + } + + /** + * Creates a new JSON and parses the specified input + * + * @param _input The bytes to be parsed in a ByteBuffer + * @param _encoding The encoding of the specified bytes + * @throws IllegalStateException If the parsing fails due to a syntax error + */ + public JSON(final ByteBuffer _input, final Charset _encoding) throws IllegalStateException { + json_init(_input, _encoding); + } + + /** + * Creates a new JSON and parses the specified input + * + * @param _input The chars to be parsed + * @throws IllegalStateException If the parsing fails due to a syntax error + */ + public JSON(final CharBuffer _input) throws IllegalStateException { + json_init(_input); + } + + /** + * Cleans this JSON and sets the type to {@link JSONVALUE_TYPE#NULL}. + * + * @return This JSON + */ + public final JSON json_init() { + root_ = new JSONVALUE(LITERAL.NULL); + return (this); + } + + /** + * Cleans this JSON and sets the type to the specified + * {@link JSONVALUE_TYPE}. + * + *
    + *
  • If the _type is a LITERAL, the literal value is used.
  • + *
  • If the _type is a {@link JSONVALUE_TYPE#JSON_NUMBER}, the value 0 is + * used.
  • + *
  • If the _type is a {@link JSONVALUE_TYPE#JSON_STRING}, the empty + * String ("") is used.
  • + *
  • If the _type is an {@link JSONVALUE_TYPE#JSON_ARRAY}, a new empty + * array will be created.
  • + *
  • If the _type is an {@link JSONVALUE_TYPE#JSON_OBJECT}, a new empty + * object will be created.
  • + *
+ * + * @param _type The {@link JSONVALUE_TYPE} to initializes this JSON to + * @return This JSON + */ + public final JSON json_init(JSONVALUE_TYPE _type) { + switch (_type) { + case NULL: + case FALSE: + case TRUE: + root_ = new JSONVALUE(LITERAL.FROM_TYPE(_type)); + break; + case JSON_NUMBER: + root_ = new JSONVALUE(0); + break; + case JSON_STRING: + root_ = new JSONVALUE(""); + break; + case JSON_ARRAY: + root_ = new JSONVALUE(_type, new ArrayList()); + break; + case JSON_OBJECT: + root_ = new JSONVALUE(_type, new HashMap()); + break; + } + return (this); + } + + /** + * Deep copies the values of the given JSON to this JSON. (No references + * will be reused) + * + * @param _other The JSON to be copied + * @return This JSON + */ + public final JSON json_init(final JSON _other) { + root_ = _other.root_.copy(); + return (this); + } + + /** + * Creates a new JSON and deep copies all values of this JSON to the new + * JSON. (No references will be reused) + * + * @return The new JSON + */ + public final JSON copy() { + return (new JSON(this)); + } + + //========================================================================== + //# # + //# Stream based initialization # + //# # + //========================================================================== + /** + * Parses the given input and replaces the current JSON with the result. + * + * @param _json The JSON to be parsed + * @return This JSON + * @throws IOException If reading fails + * @throws IllegalStateException If the parsing fails due to a syntax error + */ + public final JSON json_init(final File _json) throws IOException, IllegalStateException { + return (json_init(new BufferedReader(new FileReader(_json)))); + } + + /** + * Parses the given input and replaces the current JSON with the result. + * + * @param _json The JSON to be parsed + * @return This JSON + * @throws IOException If reading fails + * @throws IllegalStateException If the parsing fails due to a syntax error + */ + public final JSON json_init(final InputStream _json) throws IOException, IllegalStateException { + return (json_init(new BufferedReader(new InputStreamReader(_json)))); + } + + /** + * Parses the given input and replaces the current JSON with the result. + * + * @param _json The JSON to be parsed + * @return This JSON + * @throws IOException If reading fails + * @throws IllegalStateException If the parsing fails due to a syntax error + */ + public final JSON json_init(final BufferedReader _json) throws IOException, IllegalStateException { + json_init(); + try { + root_ = parse_root(() -> { + try { + return ((char) _json.read()); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }); + } catch (UncheckedIOException ex) { + throw ex.getCause(); + } + return (this); + } + + //========================================================================== + //# # + //# CharBuffer based initialization # + //# # + //========================================================================== + /** + * Parses the given input and replaces the current JSON with the result. + * + * @param _json The JSON to be parsed + * @return This JSON + * @throws IllegalStateException If the parsing fails due to a syntax error + */ + public final JSON json_init(final String _json) { + return (json_init(CharBuffer.wrap(_json))); + } + + /** + * Parses the given input and replaces the current JSON with the result. + * + * @param _json The JSON to be parsed + * @param _encoding The encoding of the specified bytes + * @return This JSON + * @throws IllegalStateException If the parsing fails due to a syntax error + */ + public final JSON json_init(final byte[] _json, final Charset _encoding) { + return (json_init(_encoding.decode(ByteBuffer.wrap(_json)))); + } + + /** + * Parses the given input and replaces the current JSON with the result. + * + * @param _json The JSON to be parsed + * @param _encoding The encoding of the specified bytes + * @return This JSON + * @throws IllegalStateException If the parsing fails due to a syntax error + */ + public final JSON json_init(final ByteBuffer _json, final Charset _encoding) { + json_init(_encoding.decode(_json)); + _json.rewind(); + return (this); + } + + /** + * Parses the given input and replaces the current JSON with the result. + * + * @param _json The JSON to be parsed + * @return This JSON + * @throws IllegalStateException If the parsing fails due to a syntax error + */ + public final JSON json_init(final CharBuffer _json) { + json_init(); + root_ = parse_root(() -> { + if (!_json.hasRemaining()) { + return (EOF); + } + return (_json.get()); + }); + return (this); + } + + //========================================================================== + //# # + //# JSON parsing methods # + //# # + //# Parse the JSON context free language according to # + //# https://www.json.org/json-en.html # + //# # + //# We parse character by character (PDA like) using the internal # + //# CHAR_SUPPLIER class. This wraps streams and buffers to a common # + //# proxy, which will provide EOF when the stream or buffer is exh- # + //# austed. # + //# # + //========================================================================== + private JSONVALUE parse_root(final CHAR_SUPPLIER _input) { + + char character; + + do { + character = _input.nextChar(); + } while (Character.isWhitespace(character)); + + if (character == EOF) { + return (new JSONVALUE(LITERAL.NULL)); + } + + RETURN_TUPLE value = parse_value(character, _input); + + if (value.returnChar != EOF && !Character.isWhitespace(value.returnChar)) { + throw new IllegalStateException("Illegal JSON: '" + value.returnValue.value + "'"); + } + + char c; + while ((c = _input.nextChar()) != EOF) { + if (!Character.isWhitespace(c)) { + throw new IllegalStateException("EOF expected. Got '" + Character.toString(c) + "' (" + (int) c + ", 0x" + Integer.toHexString(c) + ") in: '" + value.returnValue.value + "'"); + } + } + + return (value.returnValue); + } + + private RETURN_TUPLE parse_value(final char _firstChar, final CHAR_SUPPLIER _input) { + + final JSONVALUE_TYPE type = parse_valueType(_firstChar); + + switch (type) { + case NULL: + return (new RETURN_TUPLE(parse_literal(LITERAL.NULL, _firstChar, _input.nextChar(), _input.nextChar(), _input.nextChar()), EOF)); + + case TRUE: + return (new RETURN_TUPLE(parse_literal(LITERAL.TRUE, _firstChar, _input.nextChar(), _input.nextChar(), _input.nextChar()), EOF)); + + case FALSE: + return (new RETURN_TUPLE(parse_literal(LITERAL.FALSE, _firstChar, _input.nextChar(), _input.nextChar(), _input.nextChar(), _input.nextChar()), EOF)); + + case JSON_STRING: + StringParseState stringState = new StringParseState(); + + char c; + while (stringState.state != StringParseState.STATE.DONE) { + c = _input.nextChar(); + if (c == EOF) { + throw new IllegalStateException("Unexpected EOF while parsing String: '" + stringState.builder.toString() + "'"); + } + stringState.parseNext((char) c); + } + + return (new RETURN_TUPLE(new JSONVALUE(JSONVALUE_TYPE.JSON_STRING, stringState.builder.toString()), EOF)); + + case JSON_NUMBER: + NumberParseState numberState = new NumberParseState((char) _firstChar); + + char d; + while (true) { + d = _input.nextChar(); + + if (!numberState.validCharacter((char) d)) { + if (numberState.state == NumberParseState.STATE.LEADING_MINUS) { + throw new IllegalStateException("Illegal number '-'"); + } + if (numberState.state == NumberParseState.STATE.EXPONENT || numberState.state == NumberParseState.STATE.EXPONENT_SIGN) { + throw new IllegalStateException("Missing number exponent '" + numberState.builder.toString() + "'"); + } + break; + } + + numberState.parseNext((char) d); + } + + switch (numberState.type) { + case INTEGER: + return (new RETURN_TUPLE(new JSONVALUE(JSONVALUE_TYPE.JSON_NUMBER, Integer.parseInt(numberState.builder.toString())), d)); + + case LONG: + return (new RETURN_TUPLE(new JSONVALUE(JSONVALUE_TYPE.JSON_NUMBER, Long.parseLong(numberState.builder.toString())), d)); + + case DOUBLE: + return (new RETURN_TUPLE(new JSONVALUE(JSONVALUE_TYPE.JSON_NUMBER, Double.parseDouble(numberState.builder.toString())), d)); + } + + case JSON_ARRAY: + final ArrayList array = new ArrayList<>(); + + char a = _input.nextChar(); + while (true) { + + if (a == EOF) { + throw new IllegalStateException("Unexpected EOF while parsing array."); + } + + while (Character.isWhitespace(a)) { + a = _input.nextChar(); + } + + if (a == ']') { + break; + } + + RETURN_TUPLE rTuple = parse_value(a, _input); + + array.add(rTuple.returnValue); + a = rTuple.returnChar; + + if (a == EOF) { + a = _input.nextChar(); + } + + while (Character.isWhitespace(a)) { + a = _input.nextChar(); + } + + if (a != ',' && a != ']') { + throw new IllegalStateException("Expected ',' or ']' while parsing array. Got '" + Character.toString(a) + "' (" + (int) a + ", 0x" + Integer.toHexString(a) + "). Last value '" + rTuple.returnValue + "'."); + } + + if (a == ',') { + do { + a = _input.nextChar(); + } while (Character.isWhitespace(a)); + + if (a == ']') { + throw new IllegalStateException("Unexpected ']' while parsing array. Last value '" + rTuple.returnValue + "'"); + } + } + } + + return (new RETURN_TUPLE(new JSONVALUE(JSONVALUE_TYPE.JSON_ARRAY, array), EOF)); + + case JSON_OBJECT: + final HashMap object = new HashMap<>(); + + char o = _input.nextChar(); + while (true) { + + if (o == EOF) { + throw new IllegalStateException("Unexpected EOF while parsing object."); + } + + while (Character.isWhitespace(o)) { + o = _input.nextChar(); + } + + if (o == '}') { + break; + } + + RETURN_TUPLE keyTuple = parse_value(o, _input); + + if (keyTuple.returnValue.type != JSONVALUE_TYPE.JSON_STRING) { + throw new IllegalStateException("Unexpected key while parsing object. Key has to be String got: '" + keyTuple.returnValue + "'"); + } + + o = keyTuple.returnChar; + + if (o == EOF) { + o = _input.nextChar(); + } + + while (Character.isWhitespace(o)) { + o = _input.nextChar(); + } + + if (o != ':') { + throw new IllegalStateException("Expected ':' while parsing object. Got '" + Character.toString(o) + "' (" + (int) o + ", 0x" + Integer.toHexString(o) + "). Key '" + keyTuple.returnValue + "'."); + } + + do { + o = _input.nextChar(); + } while (Character.isWhitespace(o)); + + RETURN_TUPLE valueTuple = parse_value(o, _input); + + object.put((String) keyTuple.returnValue.value, valueTuple.returnValue); + o = valueTuple.returnChar; + + if (o == EOF) { + o = _input.nextChar(); + } + + while (Character.isWhitespace(o)) { + o = _input.nextChar(); + } + + if (o != ',' && o != '}') { + throw new IllegalStateException("Expected ',' or '}' while parsing object. Got '" + Character.toString(o) + "' (" + (int) o + ", 0x" + Integer.toHexString(o) + "). Key '" + keyTuple.returnValue + "'."); + } + + if (o == ',') { + do { + o = _input.nextChar(); + } while (Character.isWhitespace(o)); + + if (o == '}') { + throw new IllegalStateException("Unexpected '}' while parsing object. Key '" + keyTuple.returnValue + "'."); + } + } + } + + return (new RETURN_TUPLE(new JSONVALUE(JSONVALUE_TYPE.JSON_OBJECT, object), EOF)); + + default: + throw new IllegalStateException("Parsing failed. Invalid JSONVALUE."); + } + } + + private JSONVALUE_TYPE parse_valueType(final char _character) { + + switch (_character) { + case '{': + return (JSONVALUE_TYPE.JSON_OBJECT); + case '[': + return (JSONVALUE_TYPE.JSON_ARRAY); + case '"': + return (JSONVALUE_TYPE.JSON_STRING); + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '-': + return (JSONVALUE_TYPE.JSON_NUMBER); + case 't': + return (JSONVALUE_TYPE.TRUE); + case 'f': + return (JSONVALUE_TYPE.FALSE); + case 'n': + return (JSONVALUE_TYPE.NULL); + } + + throw new IllegalStateException("Illegal JSONVALUE starting character: '" + Character.toString(_character) + "' (" + (int) _character + ", 0x" + Integer.toHexString(_character) + ")"); + } + + private JSONVALUE parse_literal(final LITERAL _literal, final char... _chars) { + for (int i = 0; i < _literal.name.length(); i++) { + if (_literal.name.charAt(i) != _chars[i]) { + throw new IllegalStateException("Unexpected character in " + _literal + "-literal: '" + Character.toString((char) _chars[i]) + "' (" + (int) _chars[i] + ", 0x" + Integer.toHexString(_chars[i]) + ")"); + } + } + return (new JSONVALUE(_literal.type, _literal.value)); + } + + private static class StringParseState { + + public static enum STATE { + CODEPOINT, + ESCAPE, + UNICODE, + DONE + } + + private final StringBuilder builder = new StringBuilder(); + private STATE state = STATE.CODEPOINT; + private String unicodeBuffer = ""; + + private void parseNext(final char _character) { + switch (state) { + case DONE: + throw new IllegalStateException("Unexpected String parsing call: '" + builder.toString() + "'"); + case CODEPOINT: + + if (_character < 0x20) { + throw new IllegalStateException("Illegal control character '0x" + Integer.toHexString(_character) + "' in String: '" + builder.toString() + "'"); + } + + switch (_character) { + case '"': + state = STATE.DONE; + break; + case '\\': + state = STATE.ESCAPE; + break; + default: + builder.append(_character); + break; + } + break; + case ESCAPE: + switch (_character) { + case '"': + case '\\': + case '/': + builder.append(_character); + state = STATE.CODEPOINT; + break; + + case 'b': + builder.append('\b'); + state = STATE.CODEPOINT; + break; + case 'f': + builder.append('\f'); + state = STATE.CODEPOINT; + break; + case 'n': + builder.append('\n'); + state = STATE.CODEPOINT; + break; + case 'r': + builder.append('\r'); + state = STATE.CODEPOINT; + break; + case 't': + builder.append('\t'); + state = STATE.CODEPOINT; + break; + + case 'u': + state = STATE.UNICODE; + unicodeBuffer = ""; + break; + + default: + throw new IllegalStateException("Illegal escape sequence '\\" + Character.toString(_character) + "' (0x" + Integer.toHexString(_character) + ") in String: '" + builder.toString() + "'"); + } + break; + case UNICODE: + if ((_character >= 0x30 && _character <= 0x39) || (_character >= 0x65 && _character <= 0x46) || (_character >= 0x61 && _character <= 0x66)) { + //Only allow 0-9 A-F a-f + unicodeBuffer += Character.toString(_character); + if (unicodeBuffer.length() == 4) { + builder.append(Character.toString((char) Integer.parseInt(unicodeBuffer, 16))); + state = STATE.CODEPOINT; + } + } else { + throw new IllegalStateException("Illegal unicode escape sequence '\\u" + unicodeBuffer + "' in String: '" + builder.toString() + "'"); + } + break; + } + } + } + + private static class NumberParseState { + + public final static String JAVA_INT_MAX = "2147483647"; + public final static String JAVA_INT_MIN = "-2147483648"; + + public static enum STATE { + LEADING_ZERO, + LEADING_MINUS, + LEADING_DIGIT, + DIGIT, + DECIMAL_POINT, + FRACTION, + EXPONENT, + EXPONENT_SIGN, + EXPONENT_DIGIT + } + + public static enum JAVA_NUMBER_TYPE { + INTEGER, + LONG, + DOUBLE + } + + private final StringBuilder builder = new StringBuilder(); + private final boolean negative; + private STATE state; + private JAVA_NUMBER_TYPE type = JAVA_NUMBER_TYPE.INTEGER; + + public NumberParseState(final char _character) { + builder.append(_character); + + switch (_character) { + case '-': + state = STATE.LEADING_MINUS; + negative = true; + break; + case '0': + state = STATE.LEADING_ZERO; + negative = false; + break; + default: + state = STATE.LEADING_DIGIT; + negative = false; + break; + } + } + + private boolean validCharacter(final char _character) { + if (_character >= 0x30 && _character <= 0x39) { + return (state != STATE.LEADING_ZERO); + } + + if (_character == '.') { + return (state == STATE.DIGIT || state == STATE.LEADING_DIGIT || state == STATE.LEADING_ZERO); + } + + if (_character == 'e' || _character == 'E') { + return (state == STATE.DIGIT || state == STATE.LEADING_DIGIT || state == STATE.LEADING_ZERO || state == STATE.FRACTION); + } + + if (_character == '+' || _character == '-') { + return (state == STATE.EXPONENT); + } + + return (false); + } + + private void parseNext(final char _character) { + if (_character >= 0x30 && _character <= 0x39) { + switch (state) { + case LEADING_MINUS: + if (_character == '0') { + state = STATE.LEADING_ZERO; + } + //Fall through: + case LEADING_DIGIT: + builder.append(_character); + state = STATE.DIGIT; + break; + + case DIGIT: + //Check if the current number exceeds java integer + checkType(_character); + //Fall through: + case FRACTION: + case EXPONENT_DIGIT: + builder.append(_character); + break; + + case DECIMAL_POINT: + builder.append(_character); + state = STATE.FRACTION; + break; + + case EXPONENT: + case EXPONENT_SIGN: + builder.append(_character); + state = STATE.EXPONENT_DIGIT; + break; + + default: + throw new IllegalStateException("Illegal digit '" + Character.toString(_character) + "' in parsing number: '" + builder.toString() + "'"); + } + + return; + } + + switch (_character) { + case '.': + if (state == STATE.LEADING_ZERO || state == STATE.LEADING_DIGIT || state == STATE.DIGIT) { + builder.append(_character); + state = STATE.DECIMAL_POINT; + type = JAVA_NUMBER_TYPE.DOUBLE; + } else { + throw new IllegalStateException("Illegal decimal point in parsing number: '" + builder.toString() + "'"); + } + break; + case 'E': + case 'e': + if (state == STATE.DIGIT || state == STATE.LEADING_DIGIT || state == STATE.LEADING_ZERO || state == STATE.FRACTION) { + builder.append(_character); + state = STATE.EXPONENT; + type = JAVA_NUMBER_TYPE.DOUBLE; + } else { + throw new IllegalStateException("Illegal number exponent in parsing number: '" + builder.toString() + "'"); + } + break; + case '+': + case '-': + if (state == STATE.EXPONENT) { + builder.append(_character); + state = STATE.EXPONENT_SIGN; + type = JAVA_NUMBER_TYPE.DOUBLE; + } else { + throw new IllegalStateException("Illegal number exponent sign in parsing number: '" + builder.toString() + "'"); + } + break; + } + } + + private void checkType(final char _character) { + + if (type != JAVA_NUMBER_TYPE.INTEGER) { + return; + } + + String MAX_NUM = JAVA_INT_MAX; + if (negative) { + MAX_NUM = JAVA_INT_MIN; + } + + if (builder.length() + 1 > MAX_NUM.length()) { + type = JAVA_NUMBER_TYPE.LONG; + } else if (builder.length() + 1 == MAX_NUM.length()) { + boolean decided = false; + + for (int i = negative ? 1 : 0; i < builder.length(); i++) { + + if (builder.charAt(i) > MAX_NUM.charAt(i)) { + type = JAVA_NUMBER_TYPE.LONG; + decided = true; + break; + } else if (builder.charAt(i) < MAX_NUM.charAt(i)) { + decided = true; + break; + } + + } + + if (!decided && _character > MAX_NUM.charAt(MAX_NUM.length() - 1)) { + type = JAVA_NUMBER_TYPE.LONG; + } + } + } + } + + //========================================================================== + //# # + //# JSON schema validation # + //# # + //# Implement validation methods based on the ideas of # + //# https://json-schema.org/ # + //# # + //# Notice that this implementation does not contain all schema # + //# attributes at the time of this writing. # + //# This means: THIS IMPLEMENTATION DOES NOT FOLLOW AN OFFICIAL # + //# SPECIFICATION DRAFT DUE TO MISSING COMPLETENESS # + //# # + //========================================================================== + /** + * Validates this JSON based on the restrictions given in the specified + * validator. This follows a small subset of the + * https://json-schema.org/ + * definition. + * + * @param _validator The validator to use + * @return True, if the validation succeeded. False otherwise. + */ + public final boolean json_validate(final JSON _validator) { + return (validate(root_, _validator)); + } + + private boolean validate(final JSONVALUE _object, final JSON _validator) { + switch (_validator.type_get()) { + case TRUE: + case FALSE: + return ((boolean) _validator.json_get()); + case JSON_OBJECT: + break; + default: + throw new IllegalArgumentException("Illegal validator. Not a valid JSON validator (" + _validator.root_.toString() + ")"); + } + + if (_validator.json_size() == 0) { + return (true); + } + + if (!_validator.json_containsKey("type")) { + return (true); + } + + HashSet allowedTypes = new HashSet<>(); + + switch (_validator.json_getType("type")) { + case JSON_STRING: + allowedTypes.add(_validator.json_getString("type")); + break; + case JSON_ARRAY: + for (String s : _validator.json_getArray("type").iteratorCast()) { + allowedTypes.add(s); + } + break; + default: + throw new IllegalArgumentException("Unexpected validator keyword 'type' must be a String or a String array, is: (" + _validator.json_get("type") + ")"); + } + + String matchingType = validate_matchType(_object, allowedTypes); + + if (matchingType == null) { + return (false); + } + + if (!validate_type(_object, _validator, matchingType)) { + return (false); + } + + return (true); + } + + private boolean validate_type(final JSONVALUE _object, final JSON _validator, final String _type) { + switch (_type) { + case "string": + if (_validator.json_containsKey("enum")) { + boolean enumFound = false; + for (String test : _validator.json_getArray("enum").iteratorCast()) { + if (_object.equalsValue(test)) { + enumFound = true; + break; + } + } + if (!enumFound) { + return (false); + } + } + if (_validator.json_containsKey("minLength")) { + int minLength = _validator.json_getInt("minLength"); + if (((String) _object.toValue()).length() < minLength) { + return (false); + } + } + if (_validator.json_containsKey("maxLength")) { + int minLength = _validator.json_getInt("maxLength"); + if (((String) _object.toValue()).length() > minLength) { + return (false); + } + } + return (true); + case "integer": + if (_validator.json_containsKey("minimum")) { + int minimum = _validator.json_getInt("minimum"); + if (((int) _object.toValue()) < minimum) { + return (false); + } + } + if (_validator.json_containsKey("maximum")) { + int maximum = _validator.json_getInt("maximum"); + if (((int) _object.toValue()) > maximum) { + return (false); + } + } + return (true); + case "number": + if (_validator.json_containsKey("minimum")) { + double minimum; + if (_validator.json_get("minimum") instanceof Double) { + minimum = _validator.json_getDouble("minimum"); + } else { + minimum = (double) _validator.json_getInt("minimum"); + } + if (((double) _object.toValue()) < minimum) { + return (false); + } + } + if (_validator.json_containsKey("maximum")) { + double maximum; + if (_validator.json_get("maximum") instanceof Double) { + maximum = _validator.json_getDouble("maximum"); + } else { + maximum = (double) _validator.json_getInt("maximum"); + } + if (((double) _object.toValue()) > maximum) { + return (false); + } + } + return (true); + case "array": + if (_validator.json_containsKey("minItems")) { + int minItems = _validator.json_getInt("minItems"); + if (((JSON) _object.toValue()).json_size() < minItems) { + return (false); + } + } + if (_validator.json_containsKey("maxItems")) { + int maxItems = _validator.json_getInt("maxItems"); + if (((JSON) _object.toValue()).json_size() > maxItems) { + return (false); + } + } + if (_validator.json_containsKey("items")) { + for (JSONVALUE item : (ArrayList) _object.value) { + if (!validate(item, _validator.json_getObject("items"))) { + return (false); + } + } + } + return (true); + case "object": + HashMap objectEntries = ((HashMap) _object.value); + if (_validator.json_containsKey("properties")) { + for (Object entryobj : _validator.json_getObject("properties")) { + Map.Entry entry = (Map.Entry) entryobj; + if (!objectEntries.containsKey(entry.getKey())) { + continue; + } + if (!validate(objectEntries.get(entry.getKey()), entry.getValue())) { + return (false); + } + } + } + + if (_validator.json_containsKey("required")) { + for (String key : _validator.json_getArray("required").iteratorCast()) { + if (!objectEntries.containsKey(key)) { + return (false); + } + } + } + return (true); + } + return (true); + } + + private String validate_matchType(final JSONVALUE _object, final HashSet _allowedTypes) { + for (String s : _allowedTypes) { + switch (s) { + case "string": + if (_object.type == JSONVALUE_TYPE.JSON_STRING) { + return (s); + } + break; + case "integer": + if (_object.type == JSONVALUE_TYPE.JSON_NUMBER && _object.value instanceof Integer) { + return (s); + } + break; + case "number": + if (_object.type == JSONVALUE_TYPE.JSON_NUMBER) { + return (s); + } + break; + case "object": + if (_object.type == JSONVALUE_TYPE.JSON_OBJECT) { + return (s); + } + break; + case "array": + if (_object.type == JSONVALUE_TYPE.JSON_ARRAY) { + return (s); + } + break; + case "boolean": + if (_object.type == JSONVALUE_TYPE.TRUE || _object.type == JSONVALUE_TYPE.FALSE) { + return (s); + } + break; + case "null": + if (_object.type == JSONVALUE_TYPE.NULL) { + return (s); + } + break; + } + } + + return (null); + } + + //========================================================================== + //# # + //# Public General API # + //# # + //========================================================================== + @Override + public boolean equals(final Object _other) { + if (_other == null || !(_other instanceof JSON)) { + return (false); + } + + return (root_.equals(((JSON) _other).root_)); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 23 * hash + Objects.hashCode(root_); + return hash; + } + + /** + * Returns the {@link JSONVALUE_TYPE} of this JSON. + * + * @return The type of this JSON + */ + public final JSONVALUE_TYPE type_get() { + return (root_.type); + } + + //========================================================================== + //# # + //# Public Access API # + //# # + //========================================================================== + /** + * Returns the Java value of this JSON. This will be either a native Java + * type e.g. String, int, double, boolean, null OR a JSON instance, if this + * JSON is either a JSON array or a JSON object. + * + * @return The value of this JSON + */ + public final Object json_get() { + switch (root_.type) { + case JSON_ARRAY: + case JSON_OBJECT: + return (this); + default: + return (root_.value); + } + } + + @Override + public Iterator iterator() { + switch (root_.type) { + case NULL: + return (Collections.emptyIterator()); + case JSON_ARRAY: + return (new Iterator() { + final ArrayList values = (ArrayList) root_.value; + int id = 0; + + @Override + public boolean hasNext() { + return (id < values.size()); + } + + @Override + public Object next() { + return (values.get(id++).toValue()); + } + }); + case JSON_OBJECT: + return (new Iterator() { + final LinkedList keys = new LinkedList<>(((HashMap) root_.value).keySet()); + int id = 0; + + @Override + public boolean hasNext() { + return (id < keys.size()); + } + + @Override + public Map.Entry next() { + HashMap map = ((HashMap) root_.value); + String key = keys.get(id++); + return (new AbstractMap.SimpleEntry<>(key, map.get(key).toValue())); + } + }); + default: + return (Collections.singleton(root_.toValue()).iterator()); + } + } + + /** + * Returns an iterable (used in for each loops) where every object will be + * cast to the specified type T. + * + * @param The type to cast each element to + * @return An iterable with every object cast to T + * @throws ClassCastException If this JSON is a JSON object + */ + public Iterable iteratorCast() throws ClassCastException { + switch (root_.type) { + case JSON_OBJECT: + throw new ClassCastException("Can not cast iterator for JSONObject"); + default: + return (new Iterable() { + @Override + public Iterator iterator() { + return ((Iterator) JSON.this.iterator()); + } + }); + } + } + + //# # + //# # + //+++++++++++++++++++++++++++Value == Object++++++++++++++++++++++++++++++++ + /** + * Adds the given value to this JSON with the given key. This assumes that + * this JSON is either a JSON object or null. If this JSON is null, this + * JSON will be converted to a new empty JSON object. + * + * @param _key The key of the new value + * @param _value The new value + * @return This JSON + * @throws ClassCastException If this JSON is neither a JSON object nor null + */ + public final JSON json_add(final String _key, final Object _value) throws ClassCastException { + if (root_.type != JSONVALUE_TYPE.JSON_OBJECT) { + if (root_.type != JSONVALUE_TYPE.NULL) { + throw new ClassCastException("JSON is not an object. JSON is '" + root_.toString() + "'"); + } + root_ = new JSONVALUE(JSONVALUE_TYPE.JSON_OBJECT, new HashMap()); + } + + JSONVALUE newvalue = new JSONVALUE(_value); + ((HashMap) root_.value).put(_key, newvalue); + + return (this); + } + + /** + * Returns the {@link JSONVALUE_TYPE} of the value in this JSON with the + * specified key. This assumes that this JSON is a JSON object. + * + * @param _key The key of the value to check the type of + * @return The {@link JSONVALUE_TYPE} of the value with the specified key + * @throws ClassCastException If this JSON is not a JSON object + */ + public final JSONVALUE_TYPE json_getType(final String _key) throws ClassCastException { + if (root_.type != JSONVALUE_TYPE.JSON_OBJECT) { + throw new ClassCastException("JSON is not an object. JSON is '" + root_.toString() + "'"); + } + JSONVALUE value = ((HashMap) root_.value).get(_key); + return (value.type); + } + + /** + * Returns the value in this JSON with the given key. This assumes that this + * JSON is a JSON object. Returns null, if no element with the given key + * exists in the JSON object. + * + * @param _key The key of the value to be retrieved + * @return The value with the given key, or null + * @throws ClassCastException If this JSON is not a JSON object + */ + public final Object json_get(final String _key) throws ClassCastException { + if (root_.type != JSONVALUE_TYPE.JSON_OBJECT) { + throw new ClassCastException("JSON is not an object. JSON is '" + root_.toString() + "'"); + } + JSONVALUE value = ((HashMap) root_.value).get(_key); + return (value != null ? value.toValue() : null); + } + + /** + * Returns the value in this JSON with the given key and casts it to the + * given type. This assumes that this JSON is a JSON object. + * + * @param The type of the value to be retrieved (Java native or + * {@link JSON}) + * @param _key The key of the value to be retrieved + * @return The value with the given key cast to the given type + * @throws ClassCastException If this JSON is not a JSON object OR the value + * can not be cast to the given type + */ + public final T json_getCast(final String _key) throws ClassCastException { + return ((T) json_get(_key)); + } + + /** + * Returns the value in this JSON with the given key and casts it to the + * given type. This assumes that this JSON is a JSON object. + * + * @param _key The key of the value to be retrieved + * @return The value with the given key cast to the given type + * @throws ClassCastException If this JSON is not a JSON object OR the value + * can not be cast to the given type + */ + public final String json_getString(final String _key) throws ClassCastException { + return (json_getCast(_key)); + } + + /** + * Returns the value in this JSON with the given key and casts it to the + * given type. This assumes that this JSON is a JSON object. + * + * @param _key The key of the value to be retrieved + * @return The value with the given key cast to the given type + * @throws ClassCastException If this JSON is not a JSON object OR the value + * can not be cast to the given type + */ + public final int json_getInt(final String _key) throws ClassCastException { + return (json_getCast(_key)); + } + + /** + * Returns the value in this JSON with the given key and casts it to the + * given type. This assumes that this JSON is a JSON object. + * + * @param _key The key of the value to be retrieved + * @return The value with the given key cast to the given type + * @throws ClassCastException If this JSON is not a JSON object OR the value + * can not be cast to the given type + */ + public final long json_getLong(final String _key) throws ClassCastException { + return (json_getCast(_key)); + } + + /** + * Returns the value in this JSON with the given key and casts it to the + * given type. This assumes that this JSON is a JSON object. + * + * @param _key The key of the value to be retrieved + * @return The value with the given key cast to the given type + * @throws ClassCastException If this JSON is not a JSON object OR the value + * can not be cast to the given type + */ + public final double json_getDouble(final String _key) throws ClassCastException { + return (json_getCast(_key)); + } + + /** + * Returns the value in this JSON with the given key and casts it to the + * given type. This assumes that this JSON is a JSON object. + * + * @param _key The key of the value to be retrieved + * @return The value with the given key cast to the given type + * @throws ClassCastException If this JSON is not a JSON object OR the value + * can not be cast to the given type + */ + public final float json_getFloat(final String _key) throws ClassCastException { + return (this.json_getCast(_key).floatValue()); + } + + /** + * Returns the value in this JSON with the given key and casts it to the + * given type. This assumes that this JSON is a JSON object. + * + * @param _key The key of the value to be retrieved + * @return The value with the given key cast to the given type + * @throws ClassCastException If this JSON is not a JSON object OR the value + * can not be cast to the given type + */ + public final boolean json_getBoolean(final String _key) throws ClassCastException { + return (json_getCast(_key)); + } + + /** + * Returns the value in this JSON with the given key and casts it to the + * given type. This assumes that this JSON is a JSON object. + * + * @param _key The key of the value to be retrieved + * @return The value with the given key cast to the given type + * @throws ClassCastException If this JSON is not a JSON object OR the value + * can not be cast to the given type + */ + public final JSON json_getObject(final String _key) throws ClassCastException { + JSON ret = json_getCast(_key); + if (ret.root_.type != JSONVALUE_TYPE.JSON_OBJECT) { + throw new ClassCastException(_key + " is not an object."); + } + return (ret); + } + + /** + * Returns the value in this JSON with the given key and casts it to the + * given type. This assumes that this JSON is a JSON object. + * + * @param _key The key of the value to be retrieved + * @return The value with the given key cast to the given type + * @throws ClassCastException If this JSON is not a JSON object OR the value + * can not be cast to the given type + */ + public final JSON json_getArray(final String _key) throws ClassCastException { + JSON ret = json_getCast(_key); + if (ret.root_.type != JSONVALUE_TYPE.JSON_ARRAY) { + throw new ClassCastException(_key + " is not an array."); + } + return (ret); + } + + /** + * Returns whether or not a value with the given key exists in this JSON. + * This assumes that this JSON is a JSON object or null. + * + * @param _key The key to check + * @return True, if this JSON is a JSON object and a value with the + * given key exists. False otherwise. + * @throws ClassCastException If this JSON is neither a JSON object nor null + */ + public final boolean json_containsKey(final String _key) throws ClassCastException { + if (root_.type == JSONVALUE_TYPE.NULL) { + return (false); + } + + if (root_.type != JSONVALUE_TYPE.JSON_OBJECT) { + throw new ClassCastException("JSON is not an object. JSON is '" + root_.toString() + "'"); + } + + return (((HashMap) root_.value).containsKey(_key)); + } + + /** + * Removes the key/value pair with the given key from this JSON. This + * assumes that this JSON is a JSON object. If the given key is not present, + * nothing happens. + * + * @param _key The key to be removed + * @return This JSON + * @throws ClassCastException If this JSON is not a JSON object + */ + public final JSON json_removeKey(final String _key) throws ClassCastException { + if (root_.type != JSONVALUE_TYPE.JSON_OBJECT) { + throw new ClassCastException("JSON is not an object. JSON is '" + root_.toString() + "'"); + } + + ((HashMap) root_.value).remove(_key); + return (this); + } + + //# # + //# # + //+++++++++++++++++++++++++++Value == Array+++++++++++++++++++++++++++++++++ + /** + * Adds the given value to this JSON. This assumes that this JSON is either + * a JSON array or null. If this JSON is null, this JSON will be converted + * to a new empty JSON array. The value will be added to the end of the + * array. + * + * @param _value The new value + * @return This JSON + * @throws ClassCastException If this JSON is neither a JSON array nor null + */ + public final JSON json_add(final Object _value) throws ClassCastException { + if (root_.type != JSONVALUE_TYPE.JSON_ARRAY) { + if (root_.type != JSONVALUE_TYPE.NULL) { + throw new ClassCastException("JSON is not an array. JSON is '" + root_.toString() + "'"); + } + root_ = new JSONVALUE(JSONVALUE_TYPE.JSON_ARRAY, new ArrayList()); + } + + JSONVALUE newvalue = new JSONVALUE(_value); + ((ArrayList) root_.value).add(newvalue); + + return (this); + } + + /** + * Adds the given value to this JSON.This assumes that this JSON is either a + * JSON array or null. If this JSON is null, this JSON will be converted to + * a new empty JSON array. The value will be inserted at the given position. + * + * @param _key The position to insert the new value into + * @param _value The new value + * @return This JSON + * @throws ClassCastException If this JSON is neither a JSON array nor null + */ + public final JSON json_add(final int _key, final Object _value) throws ClassCastException { + if (root_.type != JSONVALUE_TYPE.JSON_ARRAY) { + if (root_.type != JSONVALUE_TYPE.NULL) { + throw new ClassCastException("JSON is not an array. JSON is '" + root_.toString() + "'"); + } + root_ = new JSONVALUE(JSONVALUE_TYPE.JSON_ARRAY, new ArrayList()); + } + + JSONVALUE newvalue = new JSONVALUE(_value); + ((ArrayList) root_.value).add(_key, newvalue); + + return (this); + } + + /** + * Returns the {@link JSONVALUE_TYPE} of the value in this JSON with the + * specified key. This assumes that this JSON is a JSON array. + * + * @param _key The index of the value to check the type of + * @return The {@link JSONVALUE_TYPE} of the value with the specified index + * @throws ClassCastException If this JSON is not a JSON array + */ + public final JSONVALUE_TYPE json_getType(final int _key) throws ClassCastException { + if (root_.type != JSONVALUE_TYPE.JSON_ARRAY) { + throw new ClassCastException("JSON is not an array. JSON is '" + root_.toString() + "'"); + } + JSONVALUE value = ((ArrayList) root_.value).get(_key); + return (value.type); + } + + /** + * Returns the value in this JSON with the given key. This assumes that this + * JSON is a JSON array. Returns null, if no element with the given key + * exists in the JSON array. + * + * @param _key The index of the value to be retrieved + * @return The value with the given index, or null + * @throws ClassCastException If this JSON is not a JSON array + */ + public final Object json_get(final int _key) throws ClassCastException { + if (root_.type != JSONVALUE_TYPE.JSON_ARRAY) { + throw new ClassCastException("JSON is not an array. JSON is '" + root_.toString() + "'"); + } + ArrayList values = ((ArrayList) root_.value); + if (_key >= values.size()) { + return (null); + } + JSONVALUE value = values.get(_key); + return (value.toValue()); + } + + /** + * Returns the value in this JSON with the given key and casts it to the + * given type. This assumes that this JSON is a JSON array. + * + * @param The type of the value to be retrieved (Java native or + * {@link JSON}) + * @param _key The index of the value to be retrieved + * @return The value with the given index cast to the given type + * @throws ClassCastException If this JSON is not a JSON array OR the value + * can not be cast to the given type + */ + public final T json_getCast(final int _key) throws ClassCastException { + return ((T) json_get(_key)); + } + + /** + * Returns the value in this JSON with the given key and casts it to the + * given type.This assumes that this JSON is a JSON array. + * + * @param _key The index of the value to be retrieved + * @return The value with the given index cast to the given type + * @throws ClassCastException If this JSON is not a JSON array OR the value + * can not be cast to the given type + */ + public final String json_getString(final int _key) throws ClassCastException { + return (json_getCast(_key)); + } + + /** + * Returns the value in this JSON with the given key and casts it to the + * given type.This assumes that this JSON is a JSON array. + * + * @param _key The index of the value to be retrieved + * @return The value with the given index cast to the given type + * @throws ClassCastException If this JSON is not a JSON array OR the value + * can not be cast to the given type + */ + public final int json_getInt(final int _key) throws ClassCastException { + return (json_getCast(_key)); + } + + /** + * Returns the value in this JSON with the given key and casts it to the + * given type.This assumes that this JSON is a JSON array. + * + * @param _key The index of the value to be retrieved + * @return The value with the given index cast to the given type + * @throws ClassCastException If this JSON is not a JSON array OR the value + * can not be cast to the given type + */ + public final long json_getLong(final int _key) throws ClassCastException { + return (json_getCast(_key)); + } + + /** + * Returns the value in this JSON with the given key and casts it to the + * given type.This assumes that this JSON is a JSON array. + * + * @param _key The index of the value to be retrieved + * @return The value with the given index cast to the given type + * @throws ClassCastException If this JSON is not a JSON array OR the value + * can not be cast to the given type + */ + public final double json_getDouble(final int _key) throws ClassCastException { + return (json_getCast(_key)); + } + + /** + * Returns the value in this JSON with the given key and casts it to the + * given type.This assumes that this JSON is a JSON array. + * + * @param _key The index of the value to be retrieved + * @return The value with the given index cast to the given type + * @throws ClassCastException If this JSON is not a JSON array OR the value + * can not be cast to the given type + */ + public final float json_getFloat(final int _key) throws ClassCastException { + return (this.json_getCast(_key).floatValue()); + } + + /** + * Returns the value in this JSON with the given key and casts it to the + * given type.This assumes that this JSON is a JSON array. + * + * @param _key The index of the value to be retrieved + * @return The value with the given index cast to the given type + * @throws ClassCastException If this JSON is not a JSON array OR the value + * can not be cast to the given type + */ + public final boolean json_getBoolean(final int _key) throws ClassCastException { + return (json_getCast(_key)); + } + + /** + * Returns the value in this JSON with the given key and casts it to the + * given type.This assumes that this JSON is a JSON array. + * + * @param _key The index of the value to be retrieved + * @return The value with the given index cast to the given type + * @throws ClassCastException If this JSON is not a JSON array OR the value + * can not be cast to the given type + */ + public final JSON json_getObject(final int _key) throws ClassCastException { + JSON ret = json_getCast(_key); + if (ret.root_.type != JSONVALUE_TYPE.JSON_OBJECT) { + throw new ClassCastException(_key + " is not an object."); + } + return (ret); + } + + /** + * Returns the value in this JSON with the given key and casts it to the + * given type.This assumes that this JSON is a JSON array. + * + * @param _key The index of the value to be retrieved + * @return The value with the given index cast to the given type + * @throws ClassCastException If this JSON is not a JSON array OR the value + * can not be cast to the given type + */ + public final JSON json_getArray(final int _key) throws ClassCastException { + JSON ret = json_getCast(_key); + if (ret.root_.type != JSONVALUE_TYPE.JSON_ARRAY) { + throw new ClassCastException(_key + " is not an array."); + } + return (ret); + } + + /** + * Returns whether or not a value with the given key exists in this JSON. + * This assumes that this JSON is a JSON array or null. + * + * @param _key The index to check + * @return True, if this JSON is a JSON array and a value with the + * given index exists. False otherwise. + * @throws ClassCastException If this JSON is neither a JSON object nor null + */ + public final boolean json_containsKey(final int _key) throws ClassCastException { + if (root_.type == JSONVALUE_TYPE.NULL) { + return (false); + } + + if (root_.type != JSONVALUE_TYPE.JSON_ARRAY) { + throw new ClassCastException("JSON is not an array. JSON is '" + root_.toString() + "'"); + } + + return (((ArrayList) root_.value).size() > _key); + } + + /** + * Removes the value with the given index from this JSON. This assumes that + * this JSON is a JSON array. If the given index is not present, nothing + * happens. + * + * @param _key The index to be removed + * @return This JSON + * @throws ClassCastException If this JSON is not a JSON array + */ + public final JSON json_removeKey(final int _key) throws ClassCastException { + if (root_.type != JSONVALUE_TYPE.JSON_ARRAY) { + throw new ClassCastException("JSON is not an array. JSON is '" + root_.toString() + "'"); + } + + ((ArrayList) root_.value).remove(_key); + return (this); + } + + /** + * Returns the index of the first occurrence of the given value in this + * JSON.This assumes that this JSON is a JSON array or null. Returns -1 if + * the given value is not present or this JSON is null. + * + * @param _value The value to get the first index of + * @return The index of the first occurrence of the specified value OR -1 + * @throws ClassCastException If this JSON is neither a JSON array nor null + */ + public final int json_indexOf(final Object _value) throws ClassCastException { + if (root_.type == JSONVALUE_TYPE.NULL) { + return (-1); + } + + if (root_.type != JSONVALUE_TYPE.JSON_ARRAY) { + throw new ClassCastException("JSON is not an array. JSON is '" + root_.toString() + "'"); + } + + ArrayList list = ((ArrayList) root_.value); + for (int i = 0; i < list.size(); i++) { + if (list.get(i).equalsValue(_value)) { + return (i); + } + } + + return (-1); + } + + /** + * Removes the first occurrence of the given value in this JSON. This + * assumes that this JSON is a JSON array or null. If the given value is not + * present in this JSON or this JSON is null, nothing happens. + * + * @param _value The value to remove + * @return This JSON + * @throws ClassCastException If this JSON is neither a JSON array nor null + */ + public final JSON json_removeValue(final Object _value) throws ClassCastException { + int index = json_indexOf(_value); + + if (index == -1) { + return (this); + } + + ((ArrayList) root_.value).remove(index); + + return (this); + } + + //# # + //# # + //++++++++++++++++++++++++Value == Collection+++++++++++++++++++++++++++++++ + /** + * Returns the number of entries in this JSON. This assumes that this JSON + * is either a JSON object, a JSON array or null. If this JSON is null, 0 is + * returned. + * + * @return The size of this JSON collection + * @throws ClassCastException If this JSON is neither a JSON object nor a + * JSON array nor null + */ + public final int json_size() throws ClassCastException { + if (root_.type == JSONVALUE_TYPE.NULL) { + return (0); + } + + if (root_.type == JSONVALUE_TYPE.JSON_OBJECT) { + return (((HashMap) root_.value).size()); + } else if (root_.type == JSONVALUE_TYPE.JSON_ARRAY) { + return (((ArrayList) root_.value).size()); + } + + throw new ClassCastException("JSON is not a collection. JSON is '" + root_.toString() + "'"); + } + + /** + * Returns whether or not the given value is present in this JSON. This + * assumes that this JSON is either a JSON object, a JSON array or null. If + * this JSON is null, false is returned. + * + * @param _value The value to check the presence of + * @return True, if this is a JSON object or array and contains the + * given value. False otherwise. + * @throws ClassCastException If this JSON is neither a JSON object nor a + * JSON array nor null + */ + public final boolean json_containsValue(final Object _value) throws ClassCastException { + switch (root_.type) { + case NULL: + return (false); + case JSON_ARRAY: + for (JSONVALUE v : ((ArrayList) root_.value)) { + if (v.equalsValue(_value)) { + return (true); + } + } + return (false); + case JSON_OBJECT: + for (Map.Entry entry : ((HashMap) root_.value).entrySet()) { + if (entry.getValue().equalsValue(_value)) { + return (true); + } + } + return (false); + default: + throw new ClassCastException("JSON is not a collection. JSON is '" + root_.toString() + "'"); + } + } + + //========================================================================== + //# # + //# Public Output API # + //# # + //========================================================================== + @Override + public String toString() { + return (json_toString(true)); + } + + /** + * Converts this JSON into a parsable String representation with or without + * added white space. + * + * @param _whitespace Whether or not white space shall be used to render the + * output human readable. + * @return This JSON as string + */ + public final String json_toString(final boolean _whitespace) { + return (root_.json_toString(_whitespace, 1)); + } + + /** + * Converts this JSON into a parsable String representation with or without + * added white space and streams the result to a given stream. + * + * @param _destination The stream to write the content to + * @param _bufferSize The buffer size + * @param _whitespaces Whether or not white space shall be used to render + * the output human readable. + * @return This JSON + * @throws IOException If stream writing fails + */ + public final JSON json_writeToStream(final OutputStream _destination, final int _bufferSize, final boolean _whitespaces) throws IOException { + byte[] buffer = new byte[_bufferSize]; + ByteBuffer content = ByteBuffer.wrap(json_toString(_whitespaces).getBytes()); + while (content.hasRemaining()) { + int readBytes = Math.min(buffer.length, content.remaining()); + content.get(buffer, 0, readBytes); + _destination.write(buffer, 0, readBytes); + _destination.flush(); + } + return (this); + } + + /** + * Converts this JSON into a parsable String representation with or without + * added white space and streams the result to a given file. + * + * @param _destination The file to write the content to + * @param _bufferSize The buffer size + * @param _whitespaces Whether or not white space shall be used to render + * the output human readable. + * @return This JSON + * @throws IOException If stream writing fails + */ + public final JSON json_writeToFile(final File _destination, final int _bufferSize, final boolean _whitespaces) throws IOException { + return (json_writeToStream(new BufferedOutputStream(new FileOutputStream(_destination)), _bufferSize, _whitespaces)); + } + +} + diff --git a/src/swap/webwall/Main.java b/src/swap/webwall/Main.java new file mode 100644 index 0000000..6f08c14 --- /dev/null +++ b/src/swap/webwall/Main.java @@ -0,0 +1,60 @@ +package swap.webwall; + +import java.io.UnsupportedEncodingException; +import java.util.Base64; + +import swap.httpd.Handler; +import swap.httpd.Header; +import swap.httpd.LoggedServerGroup; +import swap.httpd.Request; +import swap.httpd.Response; +import swap.httpd.SecureListenerThread; +import swap.httpd.ServerGroup; +import swap.template.Element; +import swap.template.StructuredPage; + +public class Main { + + public static void main(String[] args) { + ServerGroup server = new LoggedServerGroup(new Handler() { + @Override + public Response handle(Request r) { + System.err.println("Handling request for '" + r.getURL() + "'"); + for (int i = 0; i < r.countHeaders(); i++) { + Header h = r.getHeader(i); + System.err.println("Header '" + h.getKey() + "' = '" + h.getValue() + "'"); + } + if (r.hasHeader("Authorization")) { + String auth = r.getHeader("Authorization").getValue(); + if (auth.startsWith("Basic ")) { + auth = auth.substring("Basic ".length()); + byte[] credBytes = Base64.getDecoder().decode(auth); + String credString = null; + try { + credString = new String(credBytes, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new Error("Encoding problem", e); + } + String[] creds = credString.split(":"); + if (creds.length != 2) { + throw new Error("Credentials error"); + } + System.err.println("Got '" + creds[0] + "' with pw '" + creds[1] + "'"); + } + } + Response res = new Response(Response.UNAUTHORISED); //.OK); + res.addHeader("WWW-Authenticate", "Basic"); + //res.addHeader("Content-Type", "text/html"); + StructuredPage p = new StructuredPage("Testing"); + p.getBody().n("poop poop").n(new Element("p").n("poop")); + //res.setContent(p);//"TestBlah blah

Blah"); + return res; + } + }); + + server.startHTTP(8080); + SecureListenerThread slt = new SecureListenerThread(server, 4430, "C:\\Users\\Zak\\OneDrive\\Desktop\\TestDir\\testkeys", "testkeys"); + slt.start(); + } + +}