Added project files
This commit is contained in:
commit
556474db93
73
build.xml
Normal file
73
build.xml
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- You may freely edit this file. See commented blocks below for -->
|
||||||
|
<!-- some examples of how to customize the build. -->
|
||||||
|
<!-- (If you delete it and reopen the project it will be recreated.) -->
|
||||||
|
<!-- By default, only the Clean and Build commands use this build script. -->
|
||||||
|
<!-- Commands such as Run, Debug, and Test only use this build script if -->
|
||||||
|
<!-- the Compile on Save feature is turned off for the project. -->
|
||||||
|
<!-- You can turn off the Compile on Save (or Deploy on Save) setting -->
|
||||||
|
<!-- in the project's Project Properties dialog box.-->
|
||||||
|
<project name="slswap" default="default" basedir=".">
|
||||||
|
<description>Builds, tests, and runs the project slswap.</description>
|
||||||
|
<import file="nbproject/build-impl.xml"/>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
There exist several targets which are by default empty and which can be
|
||||||
|
used for execution of your tasks. These targets are usually executed
|
||||||
|
before and after some main targets. They are:
|
||||||
|
|
||||||
|
-pre-init: called before initialization of project properties
|
||||||
|
-post-init: called after initialization of project properties
|
||||||
|
-pre-compile: called before javac compilation
|
||||||
|
-post-compile: called after javac compilation
|
||||||
|
-pre-compile-single: called before javac compilation of single file
|
||||||
|
-post-compile-single: called after javac compilation of single file
|
||||||
|
-pre-compile-test: called before javac compilation of JUnit tests
|
||||||
|
-post-compile-test: called after javac compilation of JUnit tests
|
||||||
|
-pre-compile-test-single: called before javac compilation of single JUnit test
|
||||||
|
-post-compile-test-single: called after javac compilation of single JUunit test
|
||||||
|
-pre-jar: called before JAR building
|
||||||
|
-post-jar: called after JAR building
|
||||||
|
-post-clean: called after cleaning build products
|
||||||
|
|
||||||
|
(Targets beginning with '-' are not intended to be called on their own.)
|
||||||
|
|
||||||
|
Example of inserting an obfuscator after compilation could look like this:
|
||||||
|
|
||||||
|
<target name="-post-compile">
|
||||||
|
<obfuscate>
|
||||||
|
<fileset dir="${build.classes.dir}"/>
|
||||||
|
</obfuscate>
|
||||||
|
</target>
|
||||||
|
|
||||||
|
For list of available properties check the imported
|
||||||
|
nbproject/build-impl.xml file.
|
||||||
|
|
||||||
|
|
||||||
|
Another way to customize the build is by overriding existing main targets.
|
||||||
|
The targets of interest are:
|
||||||
|
|
||||||
|
-init-macrodef-javac: defines macro for javac compilation
|
||||||
|
-init-macrodef-junit: defines macro for junit execution
|
||||||
|
-init-macrodef-debug: defines macro for class debugging
|
||||||
|
-init-macrodef-java: defines macro for class execution
|
||||||
|
-do-jar: JAR building
|
||||||
|
run: execution of project
|
||||||
|
-javadoc-build: Javadoc generation
|
||||||
|
test-report: JUnit report generation
|
||||||
|
|
||||||
|
An example of overriding the target for project execution could look like this:
|
||||||
|
|
||||||
|
<target name="run" depends="slswap-impl.jar">
|
||||||
|
<exec dir="bin" executable="launcher.exe">
|
||||||
|
<arg file="${dist.jar}"/>
|
||||||
|
</exec>
|
||||||
|
</target>
|
||||||
|
|
||||||
|
Notice that the overridden target depends on the jar target and not only on
|
||||||
|
the compile target as the regular run target does. Again, for a list of available
|
||||||
|
properties which you can use, check the target you are overriding in the
|
||||||
|
nbproject/build-impl.xml file.
|
||||||
|
|
||||||
|
-->
|
||||||
|
</project>
|
3
manifest.mf
Normal file
3
manifest.mf
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Manifest-Version: 1.0
|
||||||
|
X-COMMENT: Main-Class will be added automatically by build
|
||||||
|
|
1771
nbproject/build-impl.xml
Normal file
1771
nbproject/build-impl.xml
Normal file
File diff suppressed because it is too large
Load Diff
8
nbproject/genfiles.properties
Normal file
8
nbproject/genfiles.properties
Normal file
@ -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
|
95
nbproject/project.properties
Normal file
95
nbproject/project.properties
Normal file
@ -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
|
15
nbproject/project.xml
Normal file
15
nbproject/project.xml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://www.netbeans.org/ns/project/1">
|
||||||
|
<type>org.netbeans.modules.java.j2seproject</type>
|
||||||
|
<configuration>
|
||||||
|
<data xmlns="http://www.netbeans.org/ns/j2se-project/3">
|
||||||
|
<name>slswap</name>
|
||||||
|
<source-roots>
|
||||||
|
<root id="src.dir"/>
|
||||||
|
</source-roots>
|
||||||
|
<test-roots>
|
||||||
|
<root id="test.src.dir"/>
|
||||||
|
</test-roots>
|
||||||
|
</data>
|
||||||
|
</configuration>
|
||||||
|
</project>
|
25
src/swap/auth/AuthService.java
Normal file
25
src/swap/auth/AuthService.java
Normal file
@ -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);
|
||||||
|
}*/
|
||||||
|
}
|
67
src/swap/auth/BasicCrypto.java
Normal file
67
src/swap/auth/BasicCrypto.java
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
20
src/swap/auth/Session.java
Normal file
20
src/swap/auth/Session.java
Normal file
@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
71
src/swap/data/DataAbstraction.java
Normal file
71
src/swap/data/DataAbstraction.java
Normal file
@ -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));
|
||||||
|
}
|
||||||
|
}
|
115
src/swap/data/DataConv.java
Normal file
115
src/swap/data/DataConv.java
Normal file
@ -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<String, Object> x = (Map.Entry<String, Object>)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);
|
||||||
|
}
|
||||||
|
}
|
317
src/swap/data/DataFile.java
Normal file
317
src/swap/data/DataFile.java
Normal file
@ -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<DataString, DataString> load(InputStream input) throws IOException {
|
||||||
|
HashMap<DataString,DataString> result = new HashMap<DataString, DataString>();
|
||||||
|
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<DataString, DataString> loadOrInitialise(File f) throws IOException {
|
||||||
|
if (!f.exists()) {
|
||||||
|
store(f, new HashMap<DataString, DataString>());
|
||||||
|
}
|
||||||
|
return load(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HashMap<DataString,DataString> 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<DataString,DataString> data) throws IOException {
|
||||||
|
storeString(output, "SimpleDB");
|
||||||
|
storeInt(output, 2); // Version
|
||||||
|
ArrayList<DataString> 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<DataString> sortedKeys(HashMap<DataString, ?> data) {
|
||||||
|
ArrayList<DataString> keys = new ArrayList<DataString>();
|
||||||
|
keys.addAll(data.keySet());
|
||||||
|
keys.sort(new Comparator<DataString>() {
|
||||||
|
@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<DataString, DataString> data) {
|
||||||
|
ArrayList<DataString> 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<DataString,DataString> 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<DataString, DataString> 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<DataString> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
411
src/swap/data/DataObject.java
Normal file
411
src/swap/data/DataObject.java
Normal file
@ -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<Text, DataObject> values = new HashMap<Text, DataObject>();
|
||||||
|
|
||||||
|
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<Text> result = sortedKeys(values);
|
||||||
|
return result.toArray(new Text[result.size()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ArrayList<Text> sortedKeys(HashMap<Text, ?> data) {
|
||||||
|
ArrayList<Text> keys = new ArrayList<Text>();
|
||||||
|
keys.addAll(data.keySet());
|
||||||
|
keys.sort(new Comparator<Text>() {
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
src/swap/data/DataOp.java
Normal file
5
src/swap/data/DataOp.java
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package swap.data;
|
||||||
|
|
||||||
|
public interface DataOp {
|
||||||
|
void perform(DataTransaction x);
|
||||||
|
}
|
52
src/swap/data/DataSet.java
Normal file
52
src/swap/data/DataSet.java
Normal file
@ -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<DataString> v = new DataVariable<DataString>();
|
||||||
|
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<DataString[]> v = new DataVariable<DataString[]>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
139
src/swap/data/DataString.java
Normal file
139
src/swap/data/DataString.java
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
package swap.data;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
|
||||||
|
public final class DataString implements Comparable<DataString> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
34
src/swap/data/DataTransaction.java
Normal file
34
src/swap/data/DataTransaction.java
Normal file
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
src/swap/data/DataVariable.java
Normal file
21
src/swap/data/DataVariable.java
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package swap.data;
|
||||||
|
|
||||||
|
public final class DataVariable<T> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
13
src/swap/dbtool/Main.java
Normal file
13
src/swap/dbtool/Main.java
Normal file
@ -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] + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
12
src/swap/httpd/Content.java
Normal file
12
src/swap/httpd/Content.java
Normal file
@ -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();
|
||||||
|
}
|
5
src/swap/httpd/Handler.java
Normal file
5
src/swap/httpd/Handler.java
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package swap.httpd;
|
||||||
|
|
||||||
|
public interface Handler {
|
||||||
|
Response handle(Request r);
|
||||||
|
}
|
19
src/swap/httpd/Header.java
Normal file
19
src/swap/httpd/Header.java
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
41
src/swap/httpd/HeaderSet.java
Normal file
41
src/swap/httpd/HeaderSet.java
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package swap.httpd;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
public abstract class HeaderSet {
|
||||||
|
|
||||||
|
private final ArrayList<Header> headers = new ArrayList<Header>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
44
src/swap/httpd/ListenerThread.java
Normal file
44
src/swap/httpd/ListenerThread.java
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
60
src/swap/httpd/LoggedServerGroup.java
Normal file
60
src/swap/httpd/LoggedServerGroup.java
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
37
src/swap/httpd/Request.java
Normal file
37
src/swap/httpd/Request.java
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
81
src/swap/httpd/Response.java
Normal file
81
src/swap/httpd/Response.java
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
64
src/swap/httpd/SecureListenerThread.java
Normal file
64
src/swap/httpd/SecureListenerThread.java
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
150
src/swap/httpd/ServerGroup.java
Normal file
150
src/swap/httpd/ServerGroup.java
Normal file
@ -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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
54
src/swap/library/LibraryLogin.java
Normal file
54
src/swap/library/LibraryLogin.java
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
18
src/swap/library/LibrarySection.java
Normal file
18
src/swap/library/LibrarySection.java
Normal file
@ -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() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
98
src/swap/library/LibrarySet.java
Normal file
98
src/swap/library/LibrarySet.java
Normal file
@ -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<DataString,LibrarySection> sections = new HashMap<DataString, LibrarySection>();
|
||||||
|
|
||||||
|
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<? extends LibrarySection> cl = (Class<? extends LibrarySection>) 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());
|
||||||
|
}
|
||||||
|
}
|
23
src/swap/library/LibraryShelf.java
Normal file
23
src/swap/library/LibraryShelf.java
Normal file
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
74
src/swap/log/Log.java
Normal file
74
src/swap/log/Log.java
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
27
src/swap/log/PrintStreamLog.java
Normal file
27
src/swap/log/PrintStreamLog.java
Normal file
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
9
src/swap/old/othercereal/Field.java
Normal file
9
src/swap/old/othercereal/Field.java
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package swap.old.othercereal;
|
||||||
|
|
||||||
|
public class Field {
|
||||||
|
|
||||||
|
public Field() {
|
||||||
|
// TODO Auto-generated constructor stub
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
9
src/swap/old/othercereal/Image.java
Normal file
9
src/swap/old/othercereal/Image.java
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package swap.old.othercereal;
|
||||||
|
|
||||||
|
public class Image {
|
||||||
|
|
||||||
|
public Image() {
|
||||||
|
// TODO Auto-generated constructor stub
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
11
src/swap/old/othercereal/Method.java
Normal file
11
src/swap/old/othercereal/Method.java
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package swap.old.othercereal;
|
||||||
|
|
||||||
|
public class Method {
|
||||||
|
Type type;
|
||||||
|
String name;
|
||||||
|
|
||||||
|
public Method() {
|
||||||
|
// TODO Auto-generated constructor stub
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
9
src/swap/old/othercereal/ObjectSet.java
Normal file
9
src/swap/old/othercereal/ObjectSet.java
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package swap.old.othercereal;
|
||||||
|
|
||||||
|
public class ObjectSet {
|
||||||
|
|
||||||
|
public ObjectSet() {
|
||||||
|
// TODO Auto-generated constructor stub
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
9
src/swap/old/othercereal/Type.java
Normal file
9
src/swap/old/othercereal/Type.java
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package swap.old.othercereal;
|
||||||
|
|
||||||
|
public class Type {
|
||||||
|
|
||||||
|
public Type() {
|
||||||
|
// TODO Auto-generated constructor stub
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
13
src/swap/old/othercereal/TypeSet.java
Normal file
13
src/swap/old/othercereal/TypeSet.java
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
107
src/swap/old/pojocereal/Boxer.java
Normal file
107
src/swap/old/pojocereal/Boxer.java
Normal file
@ -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<String, Integer> stringPositions = new HashMap<String, Integer>();
|
||||||
|
private HashMap<Grain, Integer> grainPositions = new HashMap<Grain, Integer>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
30
src/swap/old/pojocereal/Brand.java
Normal file
30
src/swap/old/pojocereal/Brand.java
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package swap.old.pojocereal;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
public class Brand {
|
||||||
|
private HashMap<Class<? extends Grain>, Type> classTypes = new HashMap<Class<? extends Grain>, Type>();
|
||||||
|
private HashMap<Class<? extends Grain>, Type> namedTypes = new HashMap<Class<? extends Grain>, Type>();
|
||||||
|
|
||||||
|
public Type typeOf(Grain g) throws CerealException {
|
||||||
|
return typeOfClass(g.getClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
public final synchronized Type typeOfClass(Class<? extends Grain> 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<? extends Grain> c) throws CerealException {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
29
src/swap/old/pojocereal/CerealException.java
Normal file
29
src/swap/old/pojocereal/CerealException.java
Normal file
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
57
src/swap/old/pojocereal/Field.java
Normal file
57
src/swap/old/pojocereal/Field.java
Normal file
@ -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));
|
||||||
|
}
|
||||||
|
}
|
5
src/swap/old/pojocereal/Grain.java
Normal file
5
src/swap/old/pojocereal/Grain.java
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package swap.old.pojocereal;
|
||||||
|
|
||||||
|
public abstract class Grain extends Ingredient {
|
||||||
|
|
||||||
|
}
|
9
src/swap/old/pojocereal/Ingredient.java
Normal file
9
src/swap/old/pojocereal/Ingredient.java
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package swap.old.pojocereal;
|
||||||
|
|
||||||
|
public abstract class Ingredient {
|
||||||
|
|
||||||
|
public Ingredient() {
|
||||||
|
// TODO Auto-generated constructor stub
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
13
src/swap/old/pojocereal/Type.java
Normal file
13
src/swap/old/pojocereal/Type.java
Normal file
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
43
src/swap/superdata/SuperBin.java
Normal file
43
src/swap/superdata/SuperBin.java
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
68
src/swap/superdata/SuperDoc.java
Normal file
68
src/swap/superdata/SuperDoc.java
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
46
src/swap/superdata/SuperLock.java
Normal file
46
src/swap/superdata/SuperLock.java
Normal file
@ -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).
|
||||||
|
*
|
||||||
|
* <p/> 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<String, DataFile> databases = new WeakHashMap<String, DataFile>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
312
src/swap/superdata/SuperSet.java
Normal file
312
src/swap/superdata/SuperSet.java
Normal file
@ -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.
|
||||||
|
*
|
||||||
|
* <p/> 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).
|
||||||
|
*
|
||||||
|
* <p/> 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<Boolean> isLatest = new DataVariable<Boolean>(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();
|
||||||
|
}
|
||||||
|
}
|
20
src/swap/superdata/SuperVersion.java
Normal file
20
src/swap/superdata/SuperVersion.java
Normal file
@ -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 + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
19
src/swap/template/Attribute.java
Normal file
19
src/swap/template/Attribute.java
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
116
src/swap/template/Element.java
Normal file
116
src/swap/template/Element.java
Normal file
@ -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<Node> nodes = new ArrayList<Node>();
|
||||||
|
|
||||||
|
private final ArrayList<Attribute> headers = new ArrayList<Attribute>();
|
||||||
|
|
||||||
|
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 + "</" + getTag() + ">");
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
39
src/swap/template/Node.java
Normal file
39
src/swap/template/Node.java
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
63
src/swap/template/Page.java
Normal file
63
src/swap/template/Page.java
Normal file
@ -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 ("<!doctype html>\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;
|
||||||
|
}
|
||||||
|
}
|
10
src/swap/template/StructuredPage.java
Normal file
10
src/swap/template/StructuredPage.java
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package swap.template;
|
||||||
|
|
||||||
|
public class StructuredPage extends Page {
|
||||||
|
|
||||||
|
public StructuredPage(String title) {
|
||||||
|
super(title);
|
||||||
|
// TODO Auto-generated constructor stub
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
78
src/swap/template/Text.java
Normal file
78
src/swap/template/Text.java
Normal file
@ -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<p/>\n";
|
||||||
|
} else if (((int)(char)x) != x || isGarbage((char)x) || !isEnglish((char)x)) {
|
||||||
|
r += "&#" + x + ";";
|
||||||
|
} else {
|
||||||
|
r += new String(Character.toChars(x));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
74
src/swap/testing/DBTest.java
Normal file
74
src/swap/testing/DBTest.java
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
39
src/swap/testing/SuperTest.java
Normal file
39
src/swap/testing/SuperTest.java
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
2279
src/swap/util/JSON.java
Normal file
2279
src/swap/util/JSON.java
Normal file
File diff suppressed because it is too large
Load Diff
60
src/swap/webwall/Main.java
Normal file
60
src/swap/webwall/Main.java
Normal file
@ -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);//"<html><head><title>Test</title></head><body>Blah blah <p/>Blah</body></html>");
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.startHTTP(8080);
|
||||||
|
SecureListenerThread slt = new SecureListenerThread(server, 4430, "C:\\Users\\Zak\\OneDrive\\Desktop\\TestDir\\testkeys", "testkeys");
|
||||||
|
slt.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user