diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e69de29
diff --git a/build.xml b/build.xml
new file mode 100644
index 0000000..a713c58
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/README.txt b/lib/README.txt
new file mode 100644
index 0000000..ce80d45
--- /dev/null
+++ b/lib/README.txt
@@ -0,0 +1,30 @@
+Please download and install the following libraries into this folder.
+
+https://repo1.maven.org/maven2/org/freemarker/freemarker/2.3.23/freemarker-2.3.23.jar
+
+https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.8.7/jackson-databind-2.8.7.jar
+
+https://repo1.maven.org/maven2/org/mariadb/jdbc/mariadb-java-client/1.5.8/mariadb-java-client-1.5.8.jar
+
+https://repo1.maven.org/maven2/org/nanohttpd/nanohttpd/2.3.0/nanohttpd-2.3.0.jar
+
+https://repo1.maven.org/maven2/org/nanohttpd/nanohttpd-nanolets/2.3.0/nanohttpd-nanolets-2.3.0.jar
+
+https://repo1.maven.org/maven2/net/java/dev/jna/jna/4.3.0/jna-4.3.0.jar
+
+https://repo1.maven.org/maven2/net/java/dev/jna/jna-platform/4.3.0/jna-platform-4.3.0.jar
+
+---
+
+Coursework 2 involves developing the data access layer of a web-based forum application. You can do this either on a lab machine or on your own laptop.
+
+Please follow the instructions in the PDF file, which are repeated here:
+
+Download cw2-student.zip and unzip it; this creates a folder cw2-student/.
+Go to cw2-student/lib/ and open README.txt; download the libraries indicated into the cw2-student/lib/ folder.
+In the cw2-student/ folder, run ant compile. Start the DB VM database as usual (vagrant up, ensure you can log in with mysql) and run ant run from the cw2-student/ folder.
+Browse to http://localhost:8000 and you should see "Hello world!"
+You are required to submit all files in a single zip on the Blackboard course site.
+If you need to re-submit a file, you must resubmit your full zip again, updated appropriately.
+
+For CS2 support, please use the Unit Forums (Discussion Boards) on BlackBoard, or book an appointment.
diff --git a/lib/freemarker-2.3.23.jar b/lib/freemarker-2.3.23.jar
new file mode 100644
index 0000000..82990f6
Binary files /dev/null and b/lib/freemarker-2.3.23.jar differ
diff --git a/lib/jackson-databind-2.8.7.jar b/lib/jackson-databind-2.8.7.jar
new file mode 100644
index 0000000..1d155d3
Binary files /dev/null and b/lib/jackson-databind-2.8.7.jar differ
diff --git a/lib/jna-4.3.0.jar b/lib/jna-4.3.0.jar
new file mode 100644
index 0000000..713354e
Binary files /dev/null and b/lib/jna-4.3.0.jar differ
diff --git a/lib/jna-platform-4.3.0.jar b/lib/jna-platform-4.3.0.jar
new file mode 100644
index 0000000..16d3ae6
Binary files /dev/null and b/lib/jna-platform-4.3.0.jar differ
diff --git a/lib/mariadb-java-client-1.5.8.jar b/lib/mariadb-java-client-1.5.8.jar
new file mode 100644
index 0000000..b441a52
Binary files /dev/null and b/lib/mariadb-java-client-1.5.8.jar differ
diff --git a/lib/nanohttpd-2.3.0.jar b/lib/nanohttpd-2.3.0.jar
new file mode 100644
index 0000000..497912d
Binary files /dev/null and b/lib/nanohttpd-2.3.0.jar differ
diff --git a/lib/nanohttpd-nanolets-2.3.0.jar b/lib/nanohttpd-nanolets-2.3.0.jar
new file mode 100644
index 0000000..2a4d27c
Binary files /dev/null and b/lib/nanohttpd-nanolets-2.3.0.jar differ
diff --git a/resources/gridlex.css b/resources/gridlex.css
new file mode 100644
index 0000000..e22a8da
--- /dev/null
+++ b/resources/gridlex.css
@@ -0,0 +1,4 @@
+/*! ==========================================================================
+ GRIDLEX
+ Just a Flexbox Grid System
+========================================================================== */[class*=grid]{box-sizing:border-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;margin:0 -.5rem}.col,[class*=col-]{box-sizing:border-box;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;padding:0 .5rem 1rem}.col{-webkit-flex:1 1 0%;-ms-flex:1 1 0%;flex:1 1 0%}.grid.col,.grid[class*=col-]{margin:0;padding:0}[class*=grid-][class*=-noGutter]{margin:0}[class*=grid-][class*=-noGutter]>[class*=col]{padding:0}[class*=grid-][class*=-center]{-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}[class*=grid-][class*=-right]{-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end;-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end;margin-left:auto}[class*=grid-][class*=-top]{-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start}[class*=grid-][class*=-middle]{-webkit-align-items:center;-ms-flex-align:center;align-items:center}[class*=grid-][class*=-bottom]{-webkit-align-items:flex-end;-ms-flex-align:end;align-items:flex-end}[class*=grid-][class*=-reverse]{-webkit-flex-direction:row-reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}[class*=grid-][class*=-column]{-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column}[class*=grid-][class*=-column]>[class*=col-]{-webkit-flex-basis:auto;-ms-flex-preferred-size:auto;flex-basis:auto}[class*=grid-][class*=-column-reverse]{-webkit-flex-direction:column-reverse;-ms-flex-direction:column-reverse;flex-direction:column-reverse}[class*=grid-][class*=-spaceBetween]{-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}[class*=grid-][class*=-spaceAround]{-webkit-justify-content:space-around;-ms-flex-pack:distribute;justify-content:space-around}[class*=grid-][class*=-equalHeight]>[class*=col]{display:-webkit-flex;display:-ms-flexbox;display:flex}[class*=col-][class*=-top]{-webkit-align-self:flex-start;-ms-flex-item-align:start;align-self:flex-start}[class*=col-][class*=-middle]{-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}[class*=col-][class*=-bottom]{-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end}[class*=col-][class*=-first]{-webkit-order:-1;-ms-flex-order:-1;order:-1}[class*=col-][class*=-last]{-webkit-order:1;-ms-flex-order:1;order:1}[class*=grid-1]>.col,[class*=grid-1]>[class*=col-]{-webkit-flex-basis:100%;-ms-flex-preferred-size:100%;flex-basis:100%;max-width:100%}[class*=grid-2]>.col,[class*=grid-2]>[class*=col-]{-webkit-flex-basis:50%;-ms-flex-preferred-size:50%;flex-basis:50%;max-width:50%}[class*=grid-3]>.col,[class*=grid-3]>[class*=col-]{-webkit-flex-basis:33.33333333%;-ms-flex-preferred-size:33.33333333%;flex-basis:33.33333333%;max-width:33.33333333%}[class*=grid-4]>.col,[class*=grid-4]>[class*=col-]{-webkit-flex-basis:25%;-ms-flex-preferred-size:25%;flex-basis:25%;max-width:25%}[class*=grid-5]>.col,[class*=grid-5]>[class*=col-]{-webkit-flex-basis:20%;-ms-flex-preferred-size:20%;flex-basis:20%;max-width:20%}[class*=grid-6]>.col,[class*=grid-6]>[class*=col-]{-webkit-flex-basis:16.66666667%;-ms-flex-preferred-size:16.66666667%;flex-basis:16.66666667%;max-width:16.66666667%}[class*=grid-7]>.col,[class*=grid-7]>[class*=col-]{-webkit-flex-basis:14.28571429%;-ms-flex-preferred-size:14.28571429%;flex-basis:14.28571429%;max-width:14.28571429%}[class*=grid-8]>.col,[class*=grid-8]>[class*=col-]{-webkit-flex-basis:12.5%;-ms-flex-preferred-size:12.5%;flex-basis:12.5%;max-width:12.5%}[class*=grid-9]>.col,[class*=grid-9]>[class*=col-]{-webkit-flex-basis:11.11111111%;-ms-flex-preferred-size:11.11111111%;flex-basis:11.11111111%;max-width:11.11111111%}[class*=grid-10]>.col,[class*=grid-10]>[class*=col-]{-webkit-flex-basis:10%;-ms-flex-preferred-size:10%;flex-basis:10%;max-width:10%}[class*=grid-10]>[class*=col-],[class*=grid-11]>.col{-webkit-flex-basis:9.09090909%;-ms-flex-preferred-size:9.09090909%;flex-basis:9.09090909%;max-width:9.09090909%}[class*=grid-11]>[class*=col-],[class*=grid-12]>.col{-webkit-flex-basis:8.33333333%;-ms-flex-preferred-size:8.33333333%;flex-basis:8.33333333%;max-width:8.33333333%}@media screen and (max-width:80em){[class*=_lg-1]>.col,[class*=_lg-1]>[class*=col-]{-webkit-flex-basis:100%;-ms-flex-preferred-size:100%;flex-basis:100%;max-width:100%}[class*=_lg-2]>.col,[class*=_lg-2]>[class*=col-]{-webkit-flex-basis:50%;-ms-flex-preferred-size:50%;flex-basis:50%;max-width:50%}[class*=_lg-3]>.col,[class*=_lg-3]>[class*=col-]{-webkit-flex-basis:33.33333333%;-ms-flex-preferred-size:33.33333333%;flex-basis:33.33333333%;max-width:33.33333333%}[class*=_lg-4]>.col,[class*=_lg-4]>[class*=col-]{-webkit-flex-basis:25%;-ms-flex-preferred-size:25%;flex-basis:25%;max-width:25%}[class*=_lg-5]>.col,[class*=_lg-5]>[class*=col-]{-webkit-flex-basis:20%;-ms-flex-preferred-size:20%;flex-basis:20%;max-width:20%}[class*=_lg-6]>.col,[class*=_lg-6]>[class*=col-]{-webkit-flex-basis:16.66666667%;-ms-flex-preferred-size:16.66666667%;flex-basis:16.66666667%;max-width:16.66666667%}[class*=_lg-7]>.col,[class*=_lg-7]>[class*=col-]{-webkit-flex-basis:14.28571429%;-ms-flex-preferred-size:14.28571429%;flex-basis:14.28571429%;max-width:14.28571429%}[class*=_lg-8]>.col,[class*=_lg-8]>[class*=col-]{-webkit-flex-basis:12.5%;-ms-flex-preferred-size:12.5%;flex-basis:12.5%;max-width:12.5%}[class*=_lg-9]>.col,[class*=_lg-9]>[class*=col-]{-webkit-flex-basis:11.11111111%;-ms-flex-preferred-size:11.11111111%;flex-basis:11.11111111%;max-width:11.11111111%}[class*=_lg-10]>.col,[class*=_lg-10]>[class*=col-]{-webkit-flex-basis:10%;-ms-flex-preferred-size:10%;flex-basis:10%;max-width:10%}[class*=_lg-10]>[class*=col-],[class*=_lg-11]>.col{-webkit-flex-basis:9.09090909%;-ms-flex-preferred-size:9.09090909%;flex-basis:9.09090909%;max-width:9.09090909%}[class*=_lg-11]>[class*=col-],[class*=_lg-12]>.col{-webkit-flex-basis:8.33333333%;-ms-flex-preferred-size:8.33333333%;flex-basis:8.33333333%;max-width:8.33333333%}}@media screen and (max-width:64em){[class*=_md-1]>.col,[class*=_md-1]>[class*=col-]{-webkit-flex-basis:100%;-ms-flex-preferred-size:100%;flex-basis:100%;max-width:100%}[class*=_md-2]>.col,[class*=_md-2]>[class*=col-]{-webkit-flex-basis:50%;-ms-flex-preferred-size:50%;flex-basis:50%;max-width:50%}[class*=_md-3]>.col,[class*=_md-3]>[class*=col-]{-webkit-flex-basis:33.33333333%;-ms-flex-preferred-size:33.33333333%;flex-basis:33.33333333%;max-width:33.33333333%}[class*=_md-4]>.col,[class*=_md-4]>[class*=col-]{-webkit-flex-basis:25%;-ms-flex-preferred-size:25%;flex-basis:25%;max-width:25%}[class*=_md-5]>.col,[class*=_md-5]>[class*=col-]{-webkit-flex-basis:20%;-ms-flex-preferred-size:20%;flex-basis:20%;max-width:20%}[class*=_md-6]>.col,[class*=_md-6]>[class*=col-]{-webkit-flex-basis:16.66666667%;-ms-flex-preferred-size:16.66666667%;flex-basis:16.66666667%;max-width:16.66666667%}[class*=_md-7]>.col,[class*=_md-7]>[class*=col-]{-webkit-flex-basis:14.28571429%;-ms-flex-preferred-size:14.28571429%;flex-basis:14.28571429%;max-width:14.28571429%}[class*=_md-8]>.col,[class*=_md-8]>[class*=col-]{-webkit-flex-basis:12.5%;-ms-flex-preferred-size:12.5%;flex-basis:12.5%;max-width:12.5%}[class*=_md-9]>.col,[class*=_md-9]>[class*=col-]{-webkit-flex-basis:11.11111111%;-ms-flex-preferred-size:11.11111111%;flex-basis:11.11111111%;max-width:11.11111111%}[class*=_md-10]>.col,[class*=_md-10]>[class*=col-]{-webkit-flex-basis:10%;-ms-flex-preferred-size:10%;flex-basis:10%;max-width:10%}[class*=_md-10]>[class*=col-],[class*=_md-11]>.col{-webkit-flex-basis:9.09090909%;-ms-flex-preferred-size:9.09090909%;flex-basis:9.09090909%;max-width:9.09090909%}[class*=_md-11]>[class*=col-],[class*=_md-12]>.col{-webkit-flex-basis:8.33333333%;-ms-flex-preferred-size:8.33333333%;flex-basis:8.33333333%;max-width:8.33333333%}}@media screen and (max-width:48em){[class*=_sm-1]>.col,[class*=_sm-1]>[class*=col-]{-webkit-flex-basis:100%;-ms-flex-preferred-size:100%;flex-basis:100%;max-width:100%}[class*=_sm-2]>.col,[class*=_sm-2]>[class*=col-]{-webkit-flex-basis:50%;-ms-flex-preferred-size:50%;flex-basis:50%;max-width:50%}[class*=_sm-3]>.col,[class*=_sm-3]>[class*=col-]{-webkit-flex-basis:33.33333333%;-ms-flex-preferred-size:33.33333333%;flex-basis:33.33333333%;max-width:33.33333333%}[class*=_sm-4]>.col,[class*=_sm-4]>[class*=col-]{-webkit-flex-basis:25%;-ms-flex-preferred-size:25%;flex-basis:25%;max-width:25%}[class*=_sm-5]>.col,[class*=_sm-5]>[class*=col-]{-webkit-flex-basis:20%;-ms-flex-preferred-size:20%;flex-basis:20%;max-width:20%}[class*=_sm-6]>.col,[class*=_sm-6]>[class*=col-]{-webkit-flex-basis:16.66666667%;-ms-flex-preferred-size:16.66666667%;flex-basis:16.66666667%;max-width:16.66666667%}[class*=_sm-7]>.col,[class*=_sm-7]>[class*=col-]{-webkit-flex-basis:14.28571429%;-ms-flex-preferred-size:14.28571429%;flex-basis:14.28571429%;max-width:14.28571429%}[class*=_sm-8]>.col,[class*=_sm-8]>[class*=col-]{-webkit-flex-basis:12.5%;-ms-flex-preferred-size:12.5%;flex-basis:12.5%;max-width:12.5%}[class*=_sm-9]>.col,[class*=_sm-9]>[class*=col-]{-webkit-flex-basis:11.11111111%;-ms-flex-preferred-size:11.11111111%;flex-basis:11.11111111%;max-width:11.11111111%}[class*=_sm-10]>.col,[class*=_sm-10]>[class*=col-]{-webkit-flex-basis:10%;-ms-flex-preferred-size:10%;flex-basis:10%;max-width:10%}[class*=_sm-10]>[class*=col-],[class*=_sm-11]>.col{-webkit-flex-basis:9.09090909%;-ms-flex-preferred-size:9.09090909%;flex-basis:9.09090909%;max-width:9.09090909%}[class*=_sm-11]>[class*=col-],[class*=_sm-12]>.col{-webkit-flex-basis:8.33333333%;-ms-flex-preferred-size:8.33333333%;flex-basis:8.33333333%;max-width:8.33333333%}}@media screen and (max-width:35.5em){[class*=_xs-1]>.col,[class*=_xs-1]>[class*=col-]{-webkit-flex-basis:100%;-ms-flex-preferred-size:100%;flex-basis:100%;max-width:100%}[class*=_xs-2]>.col,[class*=_xs-2]>[class*=col-]{-webkit-flex-basis:50%;-ms-flex-preferred-size:50%;flex-basis:50%;max-width:50%}[class*=_xs-3]>.col,[class*=_xs-3]>[class*=col-]{-webkit-flex-basis:33.33333333%;-ms-flex-preferred-size:33.33333333%;flex-basis:33.33333333%;max-width:33.33333333%}[class*=_xs-4]>.col,[class*=_xs-4]>[class*=col-]{-webkit-flex-basis:25%;-ms-flex-preferred-size:25%;flex-basis:25%;max-width:25%}[class*=_xs-5]>.col,[class*=_xs-5]>[class*=col-]{-webkit-flex-basis:20%;-ms-flex-preferred-size:20%;flex-basis:20%;max-width:20%}[class*=_xs-6]>.col,[class*=_xs-6]>[class*=col-]{-webkit-flex-basis:16.66666667%;-ms-flex-preferred-size:16.66666667%;flex-basis:16.66666667%;max-width:16.66666667%}[class*=_xs-7]>.col,[class*=_xs-7]>[class*=col-]{-webkit-flex-basis:14.28571429%;-ms-flex-preferred-size:14.28571429%;flex-basis:14.28571429%;max-width:14.28571429%}[class*=_xs-8]>.col,[class*=_xs-8]>[class*=col-]{-webkit-flex-basis:12.5%;-ms-flex-preferred-size:12.5%;flex-basis:12.5%;max-width:12.5%}[class*=_xs-9]>.col,[class*=_xs-9]>[class*=col-]{-webkit-flex-basis:11.11111111%;-ms-flex-preferred-size:11.11111111%;flex-basis:11.11111111%;max-width:11.11111111%}[class*=_xs-10]>.col,[class*=_xs-10]>[class*=col-]{-webkit-flex-basis:10%;-ms-flex-preferred-size:10%;flex-basis:10%;max-width:10%}[class*=_xs-10]>[class*=col-],[class*=_xs-11]>.col{-webkit-flex-basis:9.09090909%;-ms-flex-preferred-size:9.09090909%;flex-basis:9.09090909%;max-width:9.09090909%}[class*=_xs-11]>[class*=col-],[class*=_xs-12]>.col{-webkit-flex-basis:8.33333333%;-ms-flex-preferred-size:8.33333333%;flex-basis:8.33333333%;max-width:8.33333333%}}[class*=grid]>[class*=col-1]{-webkit-flex-basis:8.33333333%;-ms-flex-preferred-size:8.33333333%;flex-basis:8.33333333%;max-width:8.33333333%}[class*=grid]>[class*=col-2]{-webkit-flex-basis:16.66666667%;-ms-flex-preferred-size:16.66666667%;flex-basis:16.66666667%;max-width:16.66666667%}[class*=grid]>[class*=col-3]{-webkit-flex-basis:25%;-ms-flex-preferred-size:25%;flex-basis:25%;max-width:25%}[class*=grid]>[class*=col-4]{-webkit-flex-basis:33.33333333%;-ms-flex-preferred-size:33.33333333%;flex-basis:33.33333333%;max-width:33.33333333%}[class*=grid]>[class*=col-5]{-webkit-flex-basis:41.66666667%;-ms-flex-preferred-size:41.66666667%;flex-basis:41.66666667%;max-width:41.66666667%}[class*=grid]>[class*=col-6]{-webkit-flex-basis:50%;-ms-flex-preferred-size:50%;flex-basis:50%;max-width:50%}[class*=grid]>[class*=col-7]{-webkit-flex-basis:58.33333333%;-ms-flex-preferred-size:58.33333333%;flex-basis:58.33333333%;max-width:58.33333333%}[class*=grid]>[class*=col-8]{-webkit-flex-basis:66.66666667%;-ms-flex-preferred-size:66.66666667%;flex-basis:66.66666667%;max-width:66.66666667%}[class*=grid]>[class*=col-9]{-webkit-flex-basis:75%;-ms-flex-preferred-size:75%;flex-basis:75%;max-width:75%}[class*=grid]>[class*=col-10]{-webkit-flex-basis:83.33333333%;-ms-flex-preferred-size:83.33333333%;flex-basis:83.33333333%;max-width:83.33333333%}[class*=grid]>[class*=col-11]{-webkit-flex-basis:91.66666667%;-ms-flex-preferred-size:91.66666667%;flex-basis:91.66666667%;max-width:91.66666667%}[class*=grid]>[class*=col-12]{-webkit-flex-basis:100%;-ms-flex-preferred-size:100%;flex-basis:100%;max-width:100%}[class*=grid]>[push-left*=off-0]{margin-left:0}[class*=grid]>[push-left*=off-1]{margin-left:8.33333333%}[class*=grid]>[push-left*=off-2]{margin-left:16.66666667%}[class*=grid]>[push-left*=off-3]{margin-left:25%}[class*=grid]>[push-left*=off-4]{margin-left:33.33333333%}[class*=grid]>[push-left*=off-5]{margin-left:41.66666667%}[class*=grid]>[push-left*=off-6]{margin-left:50%}[class*=grid]>[push-left*=off-7]{margin-left:58.33333333%}[class*=grid]>[push-left*=off-8]{margin-left:66.66666667%}[class*=grid]>[push-left*=off-9]{margin-left:75%}[class*=grid]>[push-left*=off-10]{margin-left:83.33333333%}[class*=grid]>[push-left*=off-11]{margin-left:91.66666667%}[class*=grid]>[push-right*=off-0]{margin-right:0}[class*=grid]>[push-right*=off-1]{margin-right:8.33333333%}[class*=grid]>[push-right*=off-2]{margin-right:16.66666667%}[class*=grid]>[push-right*=off-3]{margin-right:25%}[class*=grid]>[push-right*=off-4]{margin-right:33.33333333%}[class*=grid]>[push-right*=off-5]{margin-right:41.66666667%}[class*=grid]>[push-right*=off-6]{margin-right:50%}[class*=grid]>[push-right*=off-7]{margin-right:58.33333333%}[class*=grid]>[push-right*=off-8]{margin-right:66.66666667%}[class*=grid]>[push-right*=off-9]{margin-right:75%}[class*=grid]>[push-right*=off-10]{margin-right:83.33333333%}[class*=grid]>[push-right*=off-11]{margin-right:91.66666667%}@media screen and (max-width:80em){[class*=grid]>[class*=_lg-1]{-webkit-flex-basis:8.33333333%;-ms-flex-preferred-size:8.33333333%;flex-basis:8.33333333%;max-width:8.33333333%}[class*=grid]>[class*=_lg-2]{-webkit-flex-basis:16.66666667%;-ms-flex-preferred-size:16.66666667%;flex-basis:16.66666667%;max-width:16.66666667%}[class*=grid]>[class*=_lg-3]{-webkit-flex-basis:25%;-ms-flex-preferred-size:25%;flex-basis:25%;max-width:25%}[class*=grid]>[class*=_lg-4]{-webkit-flex-basis:33.33333333%;-ms-flex-preferred-size:33.33333333%;flex-basis:33.33333333%;max-width:33.33333333%}[class*=grid]>[class*=_lg-5]{-webkit-flex-basis:41.66666667%;-ms-flex-preferred-size:41.66666667%;flex-basis:41.66666667%;max-width:41.66666667%}[class*=grid]>[class*=_lg-6]{-webkit-flex-basis:50%;-ms-flex-preferred-size:50%;flex-basis:50%;max-width:50%}[class*=grid]>[class*=_lg-7]{-webkit-flex-basis:58.33333333%;-ms-flex-preferred-size:58.33333333%;flex-basis:58.33333333%;max-width:58.33333333%}[class*=grid]>[class*=_lg-8]{-webkit-flex-basis:66.66666667%;-ms-flex-preferred-size:66.66666667%;flex-basis:66.66666667%;max-width:66.66666667%}[class*=grid]>[class*=_lg-9]{-webkit-flex-basis:75%;-ms-flex-preferred-size:75%;flex-basis:75%;max-width:75%}[class*=grid]>[class*=_lg-10]{-webkit-flex-basis:83.33333333%;-ms-flex-preferred-size:83.33333333%;flex-basis:83.33333333%;max-width:83.33333333%}[class*=grid]>[class*=_lg-11]{-webkit-flex-basis:91.66666667%;-ms-flex-preferred-size:91.66666667%;flex-basis:91.66666667%;max-width:91.66666667%}[class*=grid]>[class*=_lg-12]{-webkit-flex-basis:100%;-ms-flex-preferred-size:100%;flex-basis:100%;max-width:100%}[class*=grid]>[push-left*=_lg-0]{margin-left:0}[class*=grid]>[push-left*=_lg-1]{margin-left:8.33333333%}[class*=grid]>[push-left*=_lg-2]{margin-left:16.66666667%}[class*=grid]>[push-left*=_lg-3]{margin-left:25%}[class*=grid]>[push-left*=_lg-4]{margin-left:33.33333333%}[class*=grid]>[push-left*=_lg-5]{margin-left:41.66666667%}[class*=grid]>[push-left*=_lg-6]{margin-left:50%}[class*=grid]>[push-left*=_lg-7]{margin-left:58.33333333%}[class*=grid]>[push-left*=_lg-8]{margin-left:66.66666667%}[class*=grid]>[push-left*=_lg-9]{margin-left:75%}[class*=grid]>[push-left*=_lg-10]{margin-left:83.33333333%}[class*=grid]>[push-left*=_lg-11]{margin-left:91.66666667%}[class*=grid]>[push-right*=_lg-0]{margin-right:0}[class*=grid]>[push-right*=_lg-1]{margin-right:8.33333333%}[class*=grid]>[push-right*=_lg-2]{margin-right:16.66666667%}[class*=grid]>[push-right*=_lg-3]{margin-right:25%}[class*=grid]>[push-right*=_lg-4]{margin-right:33.33333333%}[class*=grid]>[push-right*=_lg-5]{margin-right:41.66666667%}[class*=grid]>[push-right*=_lg-6]{margin-right:50%}[class*=grid]>[push-right*=_lg-7]{margin-right:58.33333333%}[class*=grid]>[push-right*=_lg-8]{margin-right:66.66666667%}[class*=grid]>[push-right*=_lg-9]{margin-right:75%}[class*=grid]>[push-right*=_lg-10]{margin-right:83.33333333%}[class*=grid]>[push-right*=_lg-11]{margin-right:91.66666667%}}@media screen and (max-width:64em){[class*=grid]>[class*=_md-1]{-webkit-flex-basis:8.33333333%;-ms-flex-preferred-size:8.33333333%;flex-basis:8.33333333%;max-width:8.33333333%}[class*=grid]>[class*=_md-2]{-webkit-flex-basis:16.66666667%;-ms-flex-preferred-size:16.66666667%;flex-basis:16.66666667%;max-width:16.66666667%}[class*=grid]>[class*=_md-3]{-webkit-flex-basis:25%;-ms-flex-preferred-size:25%;flex-basis:25%;max-width:25%}[class*=grid]>[class*=_md-4]{-webkit-flex-basis:33.33333333%;-ms-flex-preferred-size:33.33333333%;flex-basis:33.33333333%;max-width:33.33333333%}[class*=grid]>[class*=_md-5]{-webkit-flex-basis:41.66666667%;-ms-flex-preferred-size:41.66666667%;flex-basis:41.66666667%;max-width:41.66666667%}[class*=grid]>[class*=_md-6]{-webkit-flex-basis:50%;-ms-flex-preferred-size:50%;flex-basis:50%;max-width:50%}[class*=grid]>[class*=_md-7]{-webkit-flex-basis:58.33333333%;-ms-flex-preferred-size:58.33333333%;flex-basis:58.33333333%;max-width:58.33333333%}[class*=grid]>[class*=_md-8]{-webkit-flex-basis:66.66666667%;-ms-flex-preferred-size:66.66666667%;flex-basis:66.66666667%;max-width:66.66666667%}[class*=grid]>[class*=_md-9]{-webkit-flex-basis:75%;-ms-flex-preferred-size:75%;flex-basis:75%;max-width:75%}[class*=grid]>[class*=_md-10]{-webkit-flex-basis:83.33333333%;-ms-flex-preferred-size:83.33333333%;flex-basis:83.33333333%;max-width:83.33333333%}[class*=grid]>[class*=_md-11]{-webkit-flex-basis:91.66666667%;-ms-flex-preferred-size:91.66666667%;flex-basis:91.66666667%;max-width:91.66666667%}[class*=grid]>[class*=_md-12]{-webkit-flex-basis:100%;-ms-flex-preferred-size:100%;flex-basis:100%;max-width:100%}[class*=grid]>[push-left*=_md-0]{margin-left:0}[class*=grid]>[push-left*=_md-1]{margin-left:8.33333333%}[class*=grid]>[push-left*=_md-2]{margin-left:16.66666667%}[class*=grid]>[push-left*=_md-3]{margin-left:25%}[class*=grid]>[push-left*=_md-4]{margin-left:33.33333333%}[class*=grid]>[push-left*=_md-5]{margin-left:41.66666667%}[class*=grid]>[push-left*=_md-6]{margin-left:50%}[class*=grid]>[push-left*=_md-7]{margin-left:58.33333333%}[class*=grid]>[push-left*=_md-8]{margin-left:66.66666667%}[class*=grid]>[push-left*=_md-9]{margin-left:75%}[class*=grid]>[push-left*=_md-10]{margin-left:83.33333333%}[class*=grid]>[push-left*=_md-11]{margin-left:91.66666667%}[class*=grid]>[push-right*=_md-0]{margin-right:0}[class*=grid]>[push-right*=_md-1]{margin-right:8.33333333%}[class*=grid]>[push-right*=_md-2]{margin-right:16.66666667%}[class*=grid]>[push-right*=_md-3]{margin-right:25%}[class*=grid]>[push-right*=_md-4]{margin-right:33.33333333%}[class*=grid]>[push-right*=_md-5]{margin-right:41.66666667%}[class*=grid]>[push-right*=_md-6]{margin-right:50%}[class*=grid]>[push-right*=_md-7]{margin-right:58.33333333%}[class*=grid]>[push-right*=_md-8]{margin-right:66.66666667%}[class*=grid]>[push-right*=_md-9]{margin-right:75%}[class*=grid]>[push-right*=_md-10]{margin-right:83.33333333%}[class*=grid]>[push-right*=_md-11]{margin-right:91.66666667%}}@media screen and (max-width:48em){[class*=grid]>[class*=_sm-1]{-webkit-flex-basis:8.33333333%;-ms-flex-preferred-size:8.33333333%;flex-basis:8.33333333%;max-width:8.33333333%}[class*=grid]>[class*=_sm-2]{-webkit-flex-basis:16.66666667%;-ms-flex-preferred-size:16.66666667%;flex-basis:16.66666667%;max-width:16.66666667%}[class*=grid]>[class*=_sm-3]{-webkit-flex-basis:25%;-ms-flex-preferred-size:25%;flex-basis:25%;max-width:25%}[class*=grid]>[class*=_sm-4]{-webkit-flex-basis:33.33333333%;-ms-flex-preferred-size:33.33333333%;flex-basis:33.33333333%;max-width:33.33333333%}[class*=grid]>[class*=_sm-5]{-webkit-flex-basis:41.66666667%;-ms-flex-preferred-size:41.66666667%;flex-basis:41.66666667%;max-width:41.66666667%}[class*=grid]>[class*=_sm-6]{-webkit-flex-basis:50%;-ms-flex-preferred-size:50%;flex-basis:50%;max-width:50%}[class*=grid]>[class*=_sm-7]{-webkit-flex-basis:58.33333333%;-ms-flex-preferred-size:58.33333333%;flex-basis:58.33333333%;max-width:58.33333333%}[class*=grid]>[class*=_sm-8]{-webkit-flex-basis:66.66666667%;-ms-flex-preferred-size:66.66666667%;flex-basis:66.66666667%;max-width:66.66666667%}[class*=grid]>[class*=_sm-9]{-webkit-flex-basis:75%;-ms-flex-preferred-size:75%;flex-basis:75%;max-width:75%}[class*=grid]>[class*=_sm-10]{-webkit-flex-basis:83.33333333%;-ms-flex-preferred-size:83.33333333%;flex-basis:83.33333333%;max-width:83.33333333%}[class*=grid]>[class*=_sm-11]{-webkit-flex-basis:91.66666667%;-ms-flex-preferred-size:91.66666667%;flex-basis:91.66666667%;max-width:91.66666667%}[class*=grid]>[class*=_sm-12]{-webkit-flex-basis:100%;-ms-flex-preferred-size:100%;flex-basis:100%;max-width:100%}[class*=grid]>[push-left*=_sm-0]{margin-left:0}[class*=grid]>[push-left*=_sm-1]{margin-left:8.33333333%}[class*=grid]>[push-left*=_sm-2]{margin-left:16.66666667%}[class*=grid]>[push-left*=_sm-3]{margin-left:25%}[class*=grid]>[push-left*=_sm-4]{margin-left:33.33333333%}[class*=grid]>[push-left*=_sm-5]{margin-left:41.66666667%}[class*=grid]>[push-left*=_sm-6]{margin-left:50%}[class*=grid]>[push-left*=_sm-7]{margin-left:58.33333333%}[class*=grid]>[push-left*=_sm-8]{margin-left:66.66666667%}[class*=grid]>[push-left*=_sm-9]{margin-left:75%}[class*=grid]>[push-left*=_sm-10]{margin-left:83.33333333%}[class*=grid]>[push-left*=_sm-11]{margin-left:91.66666667%}[class*=grid]>[push-right*=_sm-0]{margin-right:0}[class*=grid]>[push-right*=_sm-1]{margin-right:8.33333333%}[class*=grid]>[push-right*=_sm-2]{margin-right:16.66666667%}[class*=grid]>[push-right*=_sm-3]{margin-right:25%}[class*=grid]>[push-right*=_sm-4]{margin-right:33.33333333%}[class*=grid]>[push-right*=_sm-5]{margin-right:41.66666667%}[class*=grid]>[push-right*=_sm-6]{margin-right:50%}[class*=grid]>[push-right*=_sm-7]{margin-right:58.33333333%}[class*=grid]>[push-right*=_sm-8]{margin-right:66.66666667%}[class*=grid]>[push-right*=_sm-9]{margin-right:75%}[class*=grid]>[push-right*=_sm-10]{margin-right:83.33333333%}[class*=grid]>[push-right*=_sm-11]{margin-right:91.66666667%}}@media screen and (max-width:35.5em){[class*=grid]>[class*=_xs-1]{-webkit-flex-basis:8.33333333%;-ms-flex-preferred-size:8.33333333%;flex-basis:8.33333333%;max-width:8.33333333%}[class*=grid]>[class*=_xs-2]{-webkit-flex-basis:16.66666667%;-ms-flex-preferred-size:16.66666667%;flex-basis:16.66666667%;max-width:16.66666667%}[class*=grid]>[class*=_xs-3]{-webkit-flex-basis:25%;-ms-flex-preferred-size:25%;flex-basis:25%;max-width:25%}[class*=grid]>[class*=_xs-4]{-webkit-flex-basis:33.33333333%;-ms-flex-preferred-size:33.33333333%;flex-basis:33.33333333%;max-width:33.33333333%}[class*=grid]>[class*=_xs-5]{-webkit-flex-basis:41.66666667%;-ms-flex-preferred-size:41.66666667%;flex-basis:41.66666667%;max-width:41.66666667%}[class*=grid]>[class*=_xs-6]{-webkit-flex-basis:50%;-ms-flex-preferred-size:50%;flex-basis:50%;max-width:50%}[class*=grid]>[class*=_xs-7]{-webkit-flex-basis:58.33333333%;-ms-flex-preferred-size:58.33333333%;flex-basis:58.33333333%;max-width:58.33333333%}[class*=grid]>[class*=_xs-8]{-webkit-flex-basis:66.66666667%;-ms-flex-preferred-size:66.66666667%;flex-basis:66.66666667%;max-width:66.66666667%}[class*=grid]>[class*=_xs-9]{-webkit-flex-basis:75%;-ms-flex-preferred-size:75%;flex-basis:75%;max-width:75%}[class*=grid]>[class*=_xs-10]{-webkit-flex-basis:83.33333333%;-ms-flex-preferred-size:83.33333333%;flex-basis:83.33333333%;max-width:83.33333333%}[class*=grid]>[class*=_xs-11]{-webkit-flex-basis:91.66666667%;-ms-flex-preferred-size:91.66666667%;flex-basis:91.66666667%;max-width:91.66666667%}[class*=grid]>[class*=_xs-12]{-webkit-flex-basis:100%;-ms-flex-preferred-size:100%;flex-basis:100%;max-width:100%}[class*=grid]>[push-left*=_xs-0]{margin-left:0}[class*=grid]>[push-left*=_xs-1]{margin-left:8.33333333%}[class*=grid]>[push-left*=_xs-2]{margin-left:16.66666667%}[class*=grid]>[push-left*=_xs-3]{margin-left:25%}[class*=grid]>[push-left*=_xs-4]{margin-left:33.33333333%}[class*=grid]>[push-left*=_xs-5]{margin-left:41.66666667%}[class*=grid]>[push-left*=_xs-6]{margin-left:50%}[class*=grid]>[push-left*=_xs-7]{margin-left:58.33333333%}[class*=grid]>[push-left*=_xs-8]{margin-left:66.66666667%}[class*=grid]>[push-left*=_xs-9]{margin-left:75%}[class*=grid]>[push-left*=_xs-10]{margin-left:83.33333333%}[class*=grid]>[push-left*=_xs-11]{margin-left:91.66666667%}[class*=grid]>[push-right*=_xs-0]{margin-right:0}[class*=grid]>[push-right*=_xs-1]{margin-right:8.33333333%}[class*=grid]>[push-right*=_xs-2]{margin-right:16.66666667%}[class*=grid]>[push-right*=_xs-3]{margin-right:25%}[class*=grid]>[push-right*=_xs-4]{margin-right:33.33333333%}[class*=grid]>[push-right*=_xs-5]{margin-right:41.66666667%}[class*=grid]>[push-right*=_xs-6]{margin-right:50%}[class*=grid]>[push-right*=_xs-7]{margin-right:58.33333333%}[class*=grid]>[push-right*=_xs-8]{margin-right:66.66666667%}[class*=grid]>[push-right*=_xs-9]{margin-right:75%}[class*=grid]>[push-right*=_xs-10]{margin-right:83.33333333%}[class*=grid]>[push-right*=_xs-11]{margin-right:91.66666667%}}
diff --git a/resources/styles.css b/resources/styles.css
new file mode 100644
index 0000000..7e7b4eb
--- /dev/null
+++ b/resources/styles.css
@@ -0,0 +1,85 @@
+body {
+ background-color: #eee8d5; /* base2 */
+ font-family: sans-serif;
+}
+
+div.menu {
+ margin-bottom: 1ex;
+ padding: 1ex;
+ background-color: #859900;
+ color: #002b36;
+}
+
+div.menuitem a {
+ color: #002b36;
+}
+
+h1 {
+ color: #cb4b16;
+}
+
+div.section {
+ display: block;
+ background-color: #fdf6e3; /* base3 */
+ color: #2aa198;
+ padding-top: 0.5ex;
+ padding-bottom: 0.5ex;
+ margin: 1ex;
+}
+
+div.section h2 {
+ padding-left: 1ex;
+ color: #dc322f;
+}
+
+div.section p {
+ padding-left: 2ex;
+ padding-right: 2ex;
+}
+
+div.section a {
+ color: #6c71c4;
+}
+
+div.outer {
+ background-color: #073642; /* base02 */
+}
+
+div.subsection {
+ margin: 1ex;
+ padding: 0.25ex;
+ display: block;
+ background-color: #fdf6e3; /* base3 */
+ color: #859900;
+ /* border: 1px solid #268bd2; */
+}
+
+pre {
+ margin: 1ex;
+ padding: 1ex;
+ color: #d33682;
+}
+
+
+div.alt {
+ background-color: #859900;
+ color: #002b36;
+}
+
+div.alt a {
+ color: #002b36;
+}
+
+span.key {
+ width: 12em;
+ color: #859900;
+ float: left;
+}
+
+span.login {
+ float: right;
+}
+
+span.like {
+ float: right;
+}
diff --git a/resources/templates/ForumView.ftl b/resources/templates/ForumView.ftl
new file mode 100644
index 0000000..d170b4d
--- /dev/null
+++ b/resources/templates/ForumView.ftl
@@ -0,0 +1,21 @@
+<#include "header.html">
+
+
Forum: ${data.title}
+
+<#list data.topics as t>
+
+#list>
+
+
+
+<#include "footer.html">
+
diff --git a/resources/templates/ForumsView.ftl b/resources/templates/ForumsView.ftl
new file mode 100644
index 0000000..29d6f45
--- /dev/null
+++ b/resources/templates/ForumsView.ftl
@@ -0,0 +1,16 @@
+<#include "header.html">
+
+Forums
+
+<#list data.data as forum>
+
+#list>
+
+
+
+<#include "footer.html">
+
diff --git a/resources/templates/NewForumView.ftl b/resources/templates/NewForumView.ftl
new file mode 100644
index 0000000..bb1853d
--- /dev/null
+++ b/resources/templates/NewForumView.ftl
@@ -0,0 +1,16 @@
+<#include "header.html">
+
+Create new forum
+
+
+
+<#include "footer.html">
+
diff --git a/resources/templates/NewPersonView.ftl b/resources/templates/NewPersonView.ftl
new file mode 100644
index 0000000..16e1a9a
--- /dev/null
+++ b/resources/templates/NewPersonView.ftl
@@ -0,0 +1,22 @@
+<#include "header.html">
+
+Create new person
+
+
+
+<#include "footer.html">
+
diff --git a/resources/templates/NewPostView.ftl b/resources/templates/NewPostView.ftl
new file mode 100644
index 0000000..d3d3daa
--- /dev/null
+++ b/resources/templates/NewPostView.ftl
@@ -0,0 +1,16 @@
+<#include "header.html">
+
+Create new post
+
+
+
+<#include "footer.html">
+
diff --git a/resources/templates/NewTopic.ftl b/resources/templates/NewTopic.ftl
new file mode 100644
index 0000000..621ede4
--- /dev/null
+++ b/resources/templates/NewTopic.ftl
@@ -0,0 +1,18 @@
+<#include "header.html">
+
+Create new topic
+
+
+
+Title:
+
+Contents:
+
+
+
+
+
+
+
+<#include "footer.html">
+
diff --git a/resources/templates/PeopleView.ftl b/resources/templates/PeopleView.ftl
new file mode 100644
index 0000000..31a113d
--- /dev/null
+++ b/resources/templates/PeopleView.ftl
@@ -0,0 +1,18 @@
+<#include "header.html">
+
+List of people
+
+
+
+
+
+<#include "footer.html">
+
diff --git a/resources/templates/PersonView.ftl b/resources/templates/PersonView.ftl
new file mode 100644
index 0000000..ba37dac
--- /dev/null
+++ b/resources/templates/PersonView.ftl
@@ -0,0 +1,28 @@
+<#include "header.html">
+
+Person: ${data.name}
+
+
+
Information
+
+
+
Name
+
${data.name}
+
+
+
+
+
Username
+
${data.username}
+
+
+
+
+
Student id
+
${data.studentId}
+
+
+
+
+
+<#include "footer.html">
diff --git a/resources/templates/Success.ftl b/resources/templates/Success.ftl
new file mode 100644
index 0000000..c14b1aa
--- /dev/null
+++ b/resources/templates/Success.ftl
@@ -0,0 +1,15 @@
+<#include "header.html">
+
+Success
+
+
+
+
+
+
+<#include "footer.html">
+
diff --git a/resources/templates/TopicView.ftl b/resources/templates/TopicView.ftl
new file mode 100644
index 0000000..69479e7
--- /dev/null
+++ b/resources/templates/TopicView.ftl
@@ -0,0 +1,28 @@
+<#include "header.html">
+
+Topic: ${data.title?html}
+
+<#list data.posts as p>
+
+
Post #${p.postNumber} by ${p.author} at ${p.postedAt}
+<#if session??>
+#if>
+
+
+${p.text?html}
+
+
+#list>
+
+
+
+<#if session??>
+Reply
+<#else>
+Log in to reply.
+#if>
+
+
+
+<#include "footer.html">
+
diff --git a/resources/templates/footer.html b/resources/templates/footer.html
new file mode 100644
index 0000000..c2bef8b
--- /dev/null
+++ b/resources/templates/footer.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/uk/ac/bris/cs/databases/api/APIProvider.java b/src/uk/ac/bris/cs/databases/api/APIProvider.java
new file mode 100644
index 0000000..4d58cdb
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/api/APIProvider.java
@@ -0,0 +1,158 @@
+package uk.ac.bris.cs.databases.api;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This is the main interface that you have to implement.
+ *
+ * Methods have a difficulty from one to three stars that indicates
+ * very roughly how challenging they are to implement.
+ *
+ * @author csxdb
+ */
+public interface APIProvider {
+
+ /*
+ * 1 "Person" methods
+ */
+
+ /**
+ * Get a list of all users in the system as a map username -> name.
+ * @return A map with one entry per user of the form username -> name
+ * (note that usernames are unique).
+ *
+ * Difficulty: *
+ * Used by: /people (PeopleHandler)
+ *
+ * The implementation for this is provided.
+ *
+ */
+ public Result> getUsers();
+
+ /**
+ * Get a PersonView for the person with the given username.
+ * @param username - the username to search for, cannot be empty.
+ * @return If a person with the given username exists, a fully populated
+ * PersonView. Otherwise, failure (or fatal on a database error).
+ *
+ * Difficulty: *
+ * Used by: /person/:id (PersonHandler)
+ */
+ public Result getPersonView(String username);
+
+ /**
+ * Create a new person.
+ * @param name - the person's name, cannot be empty.
+ * @param username - the person's username, cannot be empty.
+ * @param studentId - the person's student id. May be either NULL if the
+ * person is not a student or a non-empty string if they are; can not be
+ * an empty string.
+ * @return Success if no person with this username existed yet and a new
+ * one was created, failure if a person with this username already exists,
+ * fatal if something else went wrong.
+ *
+ * Difficulty: **
+ * Used by: /newperson => /createperson (CreatePersonHandler)
+ *
+ * The implementation for this is provided.
+ *
+ */
+ public Result addNewPerson(String name, String username, String studentId);
+
+ /*
+ * 2 Forums only (no topics needed yet).
+ * Create some sample data in your database manually to test getForums.
+ * Then you can implement createForum and check that the two work together.
+ */
+
+ /**
+ * Get the "main page" containing a list of forums ordered alphabetically
+ * by title.
+ * @return the list of all forums; an empty list if there are no forums.
+ *
+ * Difficulty: *
+ * Used by: /forums (ForumsHandler)
+ */
+ public Result> getForums();
+
+ /**
+ * Create a new forum.
+ * @param title - the title of the forum. Must not be null or empty and
+ * no forum with this name must exist yet.
+ * @return success if the forum was created, failure if the title was
+ * null, empty or such a forum already existed; fatal on other errors.
+ *
+ * Difficulty: **
+ * Used by: /newforum => /createforum (CreateForumHandler)
+ */
+ public Result createForum(String title);
+
+ /*
+ * 3 Forums, topics, posts.
+ */
+
+ /**
+ * Get the detailed view of a single forum.
+ * @param id - the id of the forum to get.
+ * @return A view of this forum if it exists, otherwise failure.
+ *
+ * Difficulty: **
+ * Used by: /forum/:id (ForumHandler)
+ */
+ public Result getForum(int id);
+
+ /**
+ * Get a view of a topic.
+ * @param topicId - the topic to get.
+ * @return The topic view if one exists with the given id,
+ * otherwise failure or fatal on database errors.
+ *
+ * Difficulty: **
+ * Used by: /topic/:id (TopicHandler)
+ */
+ public Result getTopic(int topicId);
+
+ /**
+ * Create a post in an existing topic.
+ * @param topicId - the id of the topic to post in. Must refer to
+ * an existing topic.
+ * @param username - the name under which to post; user must exist.
+ * @param text - the content of the post, cannot be empty.
+ * @return success if the post was made, failure if any of the preconditions
+ * were not met and fatal if something else went wrong.
+ *
+ * Difficulty: ** to *** depending on schema
+ * Used by: /newpost/:id => /createpost (CreatePostHandler)
+ */
+ public Result createPost(int topicId, String username, String text);
+
+ /**
+ * Create a new topic in a forum.
+ * @param forumId - the id of the forum in which to create the topic. This
+ * forum must exist.
+ * @param username - the username under which to make this post. Must refer
+ * to an existing username.
+ * @param title - the title of this topic. Cannot be empty.
+ * @param text - the text of the initial post. Cannot be empty.
+ * @return failure if any of the preconditions are not met (forum does not
+ * exist, user does not exist, title or text empty);
+ * success if the post was created and fatal if something else went wrong.
+ *
+ * Difficulty: ***
+ * Used by: /newtopic/:id => /createtopic (CreateTopicHandler)
+ */
+ public Result createTopic(int forumId, String username, String title, String text);
+
+ /**
+ * Count the number of posts in a topic (without fetching them all).
+ * @param topicId - the topic to look at.
+ * @return The number of posts in this topic if it exists, otherwise a
+ * failure.
+ *
+ * Difficulty: *
+ * Not used in web interface.
+ */
+ public Result countPostsInTopic(int topicId);
+
+}
diff --git a/src/uk/ac/bris/cs/databases/api/ForumSummaryView.java b/src/uk/ac/bris/cs/databases/api/ForumSummaryView.java
new file mode 100644
index 0000000..9ee81be
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/api/ForumSummaryView.java
@@ -0,0 +1,37 @@
+package uk.ac.bris.cs.databases.api;
+
+import uk.ac.bris.cs.databases.util.Params;
+
+/**
+ * Simplified summary of a single forum that does not display topics.
+ * @author csxdb
+ */
+public class ForumSummaryView {
+
+ /* The title of this forum. */
+ private final String title;
+
+ /* The id of this forum. */
+ private final int id;
+
+ public ForumSummaryView(int id, String title) {
+
+ Params.cannotBeEmpty(title);
+ this.id = id;
+ this.title = title;
+ }
+
+ /**
+ * @return the title
+ */
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * @return the id
+ */
+ public int getId() {
+ return id;
+ }
+}
diff --git a/src/uk/ac/bris/cs/databases/api/ForumView.java b/src/uk/ac/bris/cs/databases/api/ForumView.java
new file mode 100644
index 0000000..8410800
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/api/ForumView.java
@@ -0,0 +1,49 @@
+package uk.ac.bris.cs.databases.api;
+
+import java.util.List;
+
+/**
+ * Detail view of a single forum.
+ * @author csxdb
+ */
+public class ForumView {
+
+ /* The id of this forum. */
+ private final int id;
+
+ /* The title of this forum. */
+ private final String title;
+
+ /* The topics in this forum, ordered by title. */
+ private final List topics;
+
+ public ForumView(int id,
+ String title,
+ List topics) {
+ this.id = id;
+ this.title = title;
+ this.topics = topics;
+ }
+
+ /**
+ * @return the title
+ */
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * @return the topics
+ */
+ public List getTopics() {
+ return topics;
+ }
+
+ /**
+ * @return the id
+ */
+ public int getId() {
+ return id;
+ }
+
+}
diff --git a/src/uk/ac/bris/cs/databases/api/PersonView.java b/src/uk/ac/bris/cs/databases/api/PersonView.java
new file mode 100644
index 0000000..3a2062b
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/api/PersonView.java
@@ -0,0 +1,52 @@
+package uk.ac.bris.cs.databases.api;
+
+import uk.ac.bris.cs.databases.util.Params;
+
+/**
+ * @author csxdb
+ */
+public class PersonView {
+
+ /* The name of the person. */
+ private final String name;
+
+ /* The username of the person. */
+ private final String username;
+
+ /* The studentId of the person or the empty string if the person does not
+ * have a student Id.
+ */
+ private final String studentId;
+
+ public PersonView(String name, String username, String studentId) {
+
+ Params.cannotBeEmpty(name);
+ Params.cannotBeEmpty(username);
+ Params.cannotBeNull(studentId);
+
+ this.name = name;
+ this.username = username;
+ this.studentId = studentId;
+ }
+
+ /**
+ * @return the name
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * @return the username
+ */
+ public String getUsername() {
+ return username;
+ }
+
+ /**
+ * @return the studentId
+ */
+ public String getStudentId() {
+ return studentId;
+ }
+}
diff --git a/src/uk/ac/bris/cs/databases/api/Result.java b/src/uk/ac/bris/cs/databases/api/Result.java
new file mode 100644
index 0000000..1837581
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/api/Result.java
@@ -0,0 +1,79 @@
+package uk.ac.bris.cs.databases.api;
+
+import uk.ac.bris.cs.databases.util.Params;
+
+/**
+ * The result of an operation that may or may not return a value, but may return
+ * an error message in case of failure.
+ * Result.success(v) - the operation completed successfully with value v.
+ * Result.failure(m) - the operation failed in a way that is to be expected
+ * (such as trying to create a new person with an username that already exists)
+ * i.e. the end user made a mistake.
+ * Result.fatal(m) - a problem that is not the user's fault, such as a broken
+ * database connection.
+ * @param The result type if the operation was successful.
+ * @author csxdb
+ */
+public class Result {
+
+ private enum Outcome { SUCCESS, FAILURE, FATAL };
+
+ private final T value;
+ private final Outcome success;
+ private final String message;
+
+ private Result(Outcome success, String message, T value) {
+ this.success = success;
+ this.message = message;
+ this.value = value;
+ }
+
+ public static Result success() {
+ return new Result(Outcome.SUCCESS, null, null);
+ }
+
+ public static Result success(U value) {
+ return new Result(Outcome.SUCCESS, null, value);
+ }
+
+ public static Result failure(String message) {
+ Params.cannotBeNull(message);
+ return new Result(Outcome.FAILURE, message, null);
+ }
+
+ public static Result fatal(String message) {
+ Params.cannotBeNull(message);
+ return new Result(Outcome.FATAL, message, null);
+ }
+
+ public boolean isSuccess() {
+ return this.success == Outcome.SUCCESS;
+ }
+
+ public T getValue() {
+ if (this.success != Outcome.SUCCESS) {
+ throw new RuntimeException();
+ }
+ return value;
+ }
+
+ public boolean isFatal() {
+ switch (this.success) {
+ case SUCCESS:
+ throw new RuntimeException();
+ case FAILURE:
+ return false;
+ case FATAL:
+ return true;
+ }
+ throw new Error();
+ }
+
+ public String getMessage() {
+ if (this.success == Outcome.SUCCESS) {
+ throw new RuntimeException();
+ } else {
+ return this.message;
+ }
+ }
+}
diff --git a/src/uk/ac/bris/cs/databases/api/SimplePostView.java b/src/uk/ac/bris/cs/databases/api/SimplePostView.java
new file mode 100644
index 0000000..84e494f
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/api/SimplePostView.java
@@ -0,0 +1,61 @@
+package uk.ac.bris.cs.databases.api;
+
+import uk.ac.bris.cs.databases.util.Params;
+
+/**
+ * View of a single post.
+ * @author csxdb
+ */
+public class SimplePostView {
+
+ /* The number of the post in the topic - the first post of each topic is
+ * always number 1.
+ */
+ private final int postNumber;
+
+ /* The name of the post author. */
+ private final String author;
+
+ /* The contents of this post. */
+ private final String text;
+
+ /* The date/time this post was made. */
+ private final String postedAt;
+
+ public SimplePostView(int postNumber, String author,
+ String text, String postedAt) {
+
+ Params.cannotBeEmpty(author);
+ Params.cannotBeEmpty(text);
+
+ this.author = author;
+ this.postNumber = postNumber;
+ this.text = text;
+ this.postedAt = postedAt;
+ }
+
+ /**
+ * @return the postNumber
+ */
+ public int getPostNumber() {
+ return postNumber;
+ }
+
+ /**
+ * @return the text
+ */
+ public String getText() {
+ return text;
+ }
+
+ /**
+ * @return the postedAt
+ */
+ public String getPostedAt() {
+ return postedAt;
+ }
+
+ public String getAuthor() {
+ return author;
+ }
+}
diff --git a/src/uk/ac/bris/cs/databases/api/SimpleTopicSummaryView.java b/src/uk/ac/bris/cs/databases/api/SimpleTopicSummaryView.java
new file mode 100644
index 0000000..f9875dd
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/api/SimpleTopicSummaryView.java
@@ -0,0 +1,50 @@
+package uk.ac.bris.cs.databases.api;
+
+import uk.ac.bris.cs.databases.util.Params;
+
+/**
+ * Simplified summary view of a topic. Just displays the title and ids.
+ * @author csxdb
+ */
+public class SimpleTopicSummaryView {
+
+ /* The id of this topic. */
+ private final int topicId;
+
+ /* The id of the forum that contains this topic. */
+ private final int forumId;
+
+ /* The title of this topic. */
+ private final String title;
+
+ public SimpleTopicSummaryView(int topicId, int forumId, String title) {
+
+ Params.cannotBeEmpty(title);
+
+ this.topicId = topicId;
+ this.forumId = forumId;
+ this.title = title;
+ }
+
+ /**
+ * @return the topicId
+ */
+ public int getTopicId() {
+ return topicId;
+ }
+
+ /**
+ * @return the forumId
+ */
+ public int getForumId() {
+ return forumId;
+ }
+
+ /**
+ * @return the title
+ */
+ public String getTitle() {
+ return title;
+ }
+
+}
diff --git a/src/uk/ac/bris/cs/databases/api/TopicView.java b/src/uk/ac/bris/cs/databases/api/TopicView.java
new file mode 100644
index 0000000..21ff935
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/api/TopicView.java
@@ -0,0 +1,50 @@
+package uk.ac.bris.cs.databases.api;
+
+import java.util.List;
+import uk.ac.bris.cs.databases.util.Params;
+
+/**
+ * Detailed view of a single topic (i.e. the posts).
+ * @author csxdb
+ */
+public class TopicView {
+
+ /* The id of this topic. */
+ private final int topicId;
+
+ /* The title of this topic. */
+ private final String title;
+
+ /* The posts in this topic, in the order that they were created. */
+ private final List posts;
+
+ public TopicView(int topicId, String title,
+ List posts) {
+
+ Params.cannotBeEmpty(title);
+ Params.cannotBeEmpty(posts);
+
+ this.topicId = topicId;
+ this.title = title;
+ this.posts = posts;
+ }
+
+ public List getPosts() {
+ return posts;
+ }
+
+ /**
+ * @return the topicId
+ */
+ public int getTopicId() {
+ return topicId;
+ }
+
+
+ /**
+ * @return the title
+ */
+ public String getTitle() {
+ return title;
+ }
+}
diff --git a/src/uk/ac/bris/cs/databases/cwk2/API.java b/src/uk/ac/bris/cs/databases/cwk2/API.java
new file mode 100644
index 0000000..8df3e71
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/cwk2/API.java
@@ -0,0 +1,159 @@
+package uk.ac.bris.cs.databases.cwk2;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import uk.ac.bris.cs.databases.api.APIProvider;
+import uk.ac.bris.cs.databases.api.ForumSummaryView;
+import uk.ac.bris.cs.databases.api.ForumView;
+import uk.ac.bris.cs.databases.api.Result;
+import uk.ac.bris.cs.databases.api.PersonView;
+import uk.ac.bris.cs.databases.api.SimplePostView;
+import uk.ac.bris.cs.databases.api.SimpleTopicSummaryView;
+import uk.ac.bris.cs.databases.api.TopicView;
+
+/**
+ *
+ * @author csxdb
+ */
+public class API implements APIProvider {
+
+ private final Connection c;
+
+ public API(Connection c) {
+ this.c = c;
+ }
+
+ /* predefined methods */
+
+ @Override
+ public Result> getUsers() {
+ try (Statement s = c.createStatement()) {
+ ResultSet r = s.executeQuery("SELECT name, username FROM Person");
+
+ Map data = new HashMap<>();
+ while (r.next()) {
+ data.put(r.getString("username"), r.getString("name"));
+ }
+
+ return Result.success(data);
+ } catch (SQLException ex) {
+ return Result.fatal("database error - " + ex.getMessage());
+ }
+ }
+
+ @Override
+ public Result addNewPerson(String name, String username, String studentId) {
+ if (studentId != null && studentId.equals("")) {
+ return Result.failure("StudentId can be null, but cannot be the empty string.");
+ }
+ if (name == null || name.equals("")) {
+ return Result.failure("Name cannot be empty.");
+ }
+ if (username == null || username.equals("")) {
+ return Result.failure("Username cannot be empty.");
+ }
+
+ try (PreparedStatement p = c.prepareStatement(
+ "SELECT count(1) AS c FROM Person WHERE username = ?"
+ )) {
+ p.setString(1, username);
+ ResultSet r = p.executeQuery();
+
+ if (r.next() && r.getInt("c") > 0) {
+ return Result.failure("A user called " + username + " already exists.");
+ }
+ } catch (SQLException e) {
+ return Result.fatal(e.getMessage());
+ }
+
+ try (PreparedStatement p = c.prepareStatement(
+ "INSERT INTO Person (name, username, stuId) VALUES (?, ?, ?)"
+ )) {
+ p.setString(1, name);
+ p.setString(2, username);
+ p.setString(3, studentId);
+ p.executeUpdate();
+
+ c.commit();
+ } catch (SQLException e) {
+ try {
+ c.rollback();
+ } catch (SQLException f) {
+ return Result.fatal("SQL error on rollback - [" + f +
+ "] from handling exception " + e);
+ }
+ return Result.fatal(e.getMessage());
+ }
+
+ return Result.success();
+ }
+
+ /* level 1 */
+ @Override
+ public Result getPersonView(String username) {
+ throw new UnsupportedOperationException("Not supported yet. 7");
+ }
+
+ @Override
+ public Result> getForums() {
+
+ try (Statement s = c.createStatement()) {
+ ResultSet r = s.executeQuery("SELECT id, title FROM Forum");
+
+ List data = new ArrayList();
+
+ while (r.next()) {
+ data.add(new ForumSummaryView(r.getInt("id"), r.getString("title")));
+ }
+
+ return Result.success(data);
+ } catch (SQLException ex) {
+ return Result.fatal("database error - " + ex.getMessage());
+ }
+ }
+
+ @Override
+ public Result countPostsInTopic(int topicId) {
+ throw new UnsupportedOperationException("Not supported yet. 1");
+ }
+
+ @Override
+ public Result getTopic(int topicId) {
+ throw new UnsupportedOperationException("Not supported yet. 2");
+ }
+
+ /* level 2 */
+
+ @Override
+ public Result createForum(String title) {
+ throw new UnsupportedOperationException("Not supported yet. 3");
+ }
+
+ @Override
+ public Result getForum(int id) {
+ throw new UnsupportedOperationException("Not supported yet. 4");
+ }
+
+ @Override
+ public Result createPost(int topicId, String username, String text) {
+ throw new UnsupportedOperationException("Not supported yet. 5");
+ }
+
+
+ /* level 3 */
+
+ @Override
+ public Result createTopic(int forumId, String username, String title, String text) {
+ throw new UnsupportedOperationException("Not supported yet. 6");
+ }
+
+}
diff --git a/src/uk/ac/bris/cs/databases/cwk2/Person.sql b/src/uk/ac/bris/cs/databases/cwk2/Person.sql
new file mode 100644
index 0000000..7432b96
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/cwk2/Person.sql
@@ -0,0 +1,11 @@
+CREATE TABLE Person (
+id INTEGER PRIMARY KEY AUTO_INCREMENT,
+name VARCHAR(100) NOT NULL,
+username VARCHAR(10) NOT NULL UNIQUE,
+stuId VARCHAR(10) NULL
+);
+
+CREATE TABLE Forum (
+id INTEGER PRIMARY KEY AUTO_INCREMENT,
+name VARCHAR(100) NOT NULL
+);
diff --git a/src/uk/ac/bris/cs/databases/util/ParameterCannotBeEmptyException.java b/src/uk/ac/bris/cs/databases/util/ParameterCannotBeEmptyException.java
new file mode 100644
index 0000000..81d8c6b
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/util/ParameterCannotBeEmptyException.java
@@ -0,0 +1,9 @@
+package uk.ac.bris.cs.databases.util;
+
+/**
+ *
+ * @author csxdb
+ */
+public class ParameterCannotBeEmptyException extends ParameterException {
+
+}
diff --git a/src/uk/ac/bris/cs/databases/util/ParameterCannotBeNullException.java b/src/uk/ac/bris/cs/databases/util/ParameterCannotBeNullException.java
new file mode 100644
index 0000000..d7a549a
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/util/ParameterCannotBeNullException.java
@@ -0,0 +1,9 @@
+package uk.ac.bris.cs.databases.util;
+
+/**
+ *
+ * @author csxdb
+ */
+public class ParameterCannotBeNullException extends ParameterException {
+
+}
diff --git a/src/uk/ac/bris/cs/databases/util/ParameterException.java b/src/uk/ac/bris/cs/databases/util/ParameterException.java
new file mode 100644
index 0000000..fc85a47
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/util/ParameterException.java
@@ -0,0 +1,9 @@
+package uk.ac.bris.cs.databases.util;
+
+/**
+ * Base class for all exceptions to catch invalid parameters.
+ * @author csxdb
+ */
+public class ParameterException extends RuntimeException {
+
+}
diff --git a/src/uk/ac/bris/cs/databases/util/Params.java b/src/uk/ac/bris/cs/databases/util/Params.java
new file mode 100644
index 0000000..6aa7119
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/util/Params.java
@@ -0,0 +1,30 @@
+package uk.ac.bris.cs.databases.util;
+
+import java.util.Collection;
+
+/**
+ *
+ * @author csxdb
+ */
+public class Params {
+
+ public static void cannotBeNull(Object o) {
+ if (o == null) {
+ throw new ParameterCannotBeNullException();
+ }
+ }
+
+ public static void cannotBeEmpty(String s) {
+ Params.cannotBeNull(s);
+ if (s.equals("")) {
+ throw new ParameterCannotBeEmptyException();
+ }
+ }
+
+ public static void cannotBeEmpty(Collection c) {
+ Params.cannotBeNull(c);
+ if (c.isEmpty()) {
+ throw new ParameterCannotBeEmptyException();
+ }
+ }
+}
diff --git a/src/uk/ac/bris/cs/databases/web/.idea/misc.xml b/src/uk/ac/bris/cs/databases/web/.idea/misc.xml
new file mode 100644
index 0000000..536199e
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/web/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/uk/ac/bris/cs/databases/web/.idea/modules.xml b/src/uk/ac/bris/cs/databases/web/.idea/modules.xml
new file mode 100644
index 0000000..f589ca3
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/web/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/uk/ac/bris/cs/databases/web/.idea/web.iml b/src/uk/ac/bris/cs/databases/web/.idea/web.iml
new file mode 100644
index 0000000..d6ebd48
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/web/.idea/web.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/uk/ac/bris/cs/databases/web/.idea/workspace.xml b/src/uk/ac/bris/cs/databases/web/.idea/workspace.xml
new file mode 100644
index 0000000..31bf47d
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/web/.idea/workspace.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1588343554394
+
+
+ 1588343554394
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/uk/ac/bris/cs/databases/web/AbstractHandler.java b/src/uk/ac/bris/cs/databases/web/AbstractHandler.java
new file mode 100644
index 0000000..143c8e6
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/web/AbstractHandler.java
@@ -0,0 +1,185 @@
+package uk.ac.bris.cs.databases.web;
+
+import fi.iki.elonen.NanoHTTPD;
+import fi.iki.elonen.router.RouterNanoHTTPD;
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import uk.ac.bris.cs.databases.api.Result;
+
+/**
+ * Code shared across web handlers.
+ * @author csxdb
+ */
+public abstract class AbstractHandler extends RouterNanoHTTPD.DefaultHandler {
+
+ public class ValueHolder {
+ private final String value;
+
+ public ValueHolder(String value) {
+ this.value = value;
+ }
+
+ /**
+ * @return the value
+ */
+ public String getValue() {
+ return value;
+ }
+
+ }
+
+ public abstract View render(RouterNanoHTTPD.UriResource uriResource,
+ Map params,
+ NanoHTTPD.IHTTPSession session);
+
+ @Override public String getMimeType() {
+ return "text/html";
+ }
+
+ @Override public NanoHTTPD.Response.IStatus getStatus() {
+ throw new RuntimeException("Should not happen.");
+ // return NanoHTTPD.Response.Status.OK;
+ }
+
+ @Override
+ public String getText() {
+ throw new RuntimeException("Should not happen - using get/post");
+ }
+
+
+
+ class Status implements NanoHTTPD.Response.IStatus {
+
+ private final int code;
+
+ public Status(int code) {
+ this.code = code;
+ }
+
+ @Override public String getDescription() {
+ switch (code) {
+ case 200: return "OK";
+ case 404: return "Not found";
+ case 500: return "Internal error";
+ default: return "OTHER"; // naughty
+ }
+ }
+
+ @Override public int getRequestStatus() {
+ return code;
+ }
+
+ }
+
+ /** Implement this to work with cookies. */
+ void handleCookies(NanoHTTPD.IHTTPSession session) {}
+
+ private NanoHTTPD.Response handle(RouterNanoHTTPD.UriResource uriResource,
+ Map urlParams,
+ NanoHTTPD.IHTTPSession session) {
+ View v = render(uriResource, urlParams, session);
+ handleCookies(session);
+ NanoHTTPD.Response r = NanoHTTPD.newFixedLengthResponse(
+ new Status(v.getCode()),
+ getMimeType(),
+ v.getContents());
+ return r;
+ }
+
+ @Override
+ public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource,
+ Map urlParams,
+ NanoHTTPD.IHTTPSession session) {
+
+ return handle(uriResource, urlParams, session);
+ }
+
+ @Override
+ public NanoHTTPD.Response post(RouterNanoHTTPD.UriResource uriResource,
+ Map urlParams,
+ NanoHTTPD.IHTTPSession session) {
+
+ return handle(uriResource, urlParams, session);
+ }
+
+ Map parseQuery(String URIParams) {
+ if (URIParams == null) {
+ return new HashMap<>();
+ }
+
+ // stackoverflow 11640025
+ Map result = new HashMap<>();
+ for (String param : URIParams.split("&")) {
+ String pair[] = param.split("=");
+ if (pair.length>1) {
+ result.put(pair[0], pair[1]);
+ }else{
+ result.put(pair[0], "");
+ }
+ }
+ return result;
+ }
+
+ View renderView(String template, Object data, Object state) {
+ Map viewdata = new HashMap<>();
+ viewdata.put("data", data);
+ viewdata.put("session", state);
+
+ Configuration c = ApplicationContext.getInstance().getTemplateConfiguration();
+
+ Template t;
+ try {
+ t = c.getTemplate(template);
+ } catch (Exception e) {
+ return new View(500, "Template error - " + e.getMessage());
+ }
+
+ StringWriter w = new StringWriter();
+ try {
+ t.process(viewdata, w);
+ } catch (TemplateException | IOException e) {
+ return new View(500, "Rendering error - " + e.getMessage());
+ }
+
+ return new View(200, w.toString());
+ }
+
+ public static class ListWrapper {
+ private final List l;
+
+ public static Result> wrap(Result> r) {
+ if (r.isSuccess()) {
+ return Result.success(new ListWrapper(r.getValue()));
+ } else {
+ // throw new RuntimeException("Trying to list-wrap an error.");
+
+ /*
+ * The wonderful thing about monads is
+ * that monads are a wonderful thing.
+ */
+ if (r.isFatal()) {
+ return Result.fatal(r.getMessage());
+ } else {
+ return Result.failure(r.getMessage());
+ }
+ }
+ }
+
+ public ListWrapper(List l) {
+ this.l = l;
+ }
+
+ public List getData() { return l; }
+ }
+
+ ListWrapper wrap(List l) {
+ return new ListWrapper(l);
+ }
+
+}
diff --git a/src/uk/ac/bris/cs/databases/web/AbstractPostHandler.java b/src/uk/ac/bris/cs/databases/web/AbstractPostHandler.java
new file mode 100644
index 0000000..84d412e
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/web/AbstractPostHandler.java
@@ -0,0 +1,82 @@
+package uk.ac.bris.cs.databases.web;
+
+import fi.iki.elonen.NanoHTTPD;
+import fi.iki.elonen.NanoHTTPD.Method;
+import fi.iki.elonen.router.RouterNanoHTTPD;
+import java.io.IOException;
+import java.util.Map;
+import uk.ac.bris.cs.databases.api.Result;
+
+/**
+ * Base class for POST (new topic / forum / post) handlers.
+ *
+ * @author David
+ */
+public abstract class AbstractPostHandler extends AbstractHandler {
+
+ public class ValueHolder {
+ private final String value;
+
+ public ValueHolder(String value) {
+ this.value = value;
+ }
+
+ /**
+ * @return the value
+ */
+ public String getValue() {
+ return value;
+ }
+
+ }
+
+ public class RenderPair {
+ final String template;
+ final Result data;
+
+ public RenderPair(String template, Result data) {
+ this.template = template;
+ this.data = data;
+ }
+ }
+
+ public abstract RenderPair handlePost(Map params,
+ NanoHTTPD.IHTTPSession session);
+
+ @Override
+ public View render(RouterNanoHTTPD.UriResource uriResource,
+ Map params,
+ NanoHTTPD.IHTTPSession session) {
+
+ Method method = session.getMethod();
+
+ if (!method.equals(Method.POST)) {
+ return new View(400, "Error - expected POST request, got " + method);
+ }
+
+ Map m = session.getParms();
+ try {
+ session.parseBody(m);
+ } catch (NanoHTTPD.ResponseException | IOException e) {
+ return new View(500, "Exception handling POST - " + e.getMessage());
+ }
+
+ System.out.println("[AbstractPostHandler] render " + uriResource.getUri());
+ for (Map.Entry e : m.entrySet()) {
+ System.out.println("param " + e.getKey() + " => " + e.getValue());
+ }
+ RenderPair rp = handlePost(m, session);
+
+ NanoHTTPD.CookieHandler h = session.getCookies();
+ String user = h.read("user");
+
+ if (rp.data.isSuccess()) {
+ return renderView(rp.template, rp.data.getValue(), user);
+ } else if (rp.data.isFatal()) {
+ return new View(500, "Fatal error - " + rp.data.getMessage());
+ } else {
+ return new View(400, "Error - " + rp.data.getMessage());
+ }
+ }
+
+}
diff --git a/src/uk/ac/bris/cs/databases/web/ApplicationContext.java b/src/uk/ac/bris/cs/databases/web/ApplicationContext.java
new file mode 100644
index 0000000..1d0ba38
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/web/ApplicationContext.java
@@ -0,0 +1,51 @@
+package uk.ac.bris.cs.databases.web;
+
+import freemarker.template.Configuration;
+import uk.ac.bris.cs.databases.api.APIProvider;
+
+/**
+ *
+ * @author csxdb
+ */
+public class ApplicationContext {
+
+ public static ApplicationContext instance = new ApplicationContext();
+ private ApplicationContext() {}
+
+ private APIProvider api;
+
+ private Configuration templateConfiguration;
+
+
+ public static ApplicationContext getInstance() {
+ return instance;
+ }
+
+ /**
+ * @return the api
+ */
+ public APIProvider getApi() {
+ return api;
+ }
+
+ /**
+ * @param api the api to set
+ */
+ public void setApi(APIProvider api) {
+ this.api = api;
+ }
+
+ /**
+ * @return the templateConfiguration
+ */
+ public Configuration getTemplateConfiguration() {
+ return templateConfiguration;
+ }
+
+ /**
+ * @param templateConfiguration the templateConfiguration to set
+ */
+ public void setTemplateConfiguration(Configuration templateConfiguration) {
+ this.templateConfiguration = templateConfiguration;
+ }
+}
diff --git a/src/uk/ac/bris/cs/databases/web/CreateForumHandler.java b/src/uk/ac/bris/cs/databases/web/CreateForumHandler.java
new file mode 100644
index 0000000..e05b5b9
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/web/CreateForumHandler.java
@@ -0,0 +1,36 @@
+package uk.ac.bris.cs.databases.web;
+
+import fi.iki.elonen.NanoHTTPD;
+import java.util.Map;
+import uk.ac.bris.cs.databases.api.APIProvider;
+import uk.ac.bris.cs.databases.api.Result;
+
+/**
+ * Create a new forum.
+ * path: POST /createforum [title]
+ *
+ * @author csxdb
+ */
+public class CreateForumHandler extends AbstractPostHandler {
+
+ @Override
+ public RenderPair handlePost(Map params,
+ NanoHTTPD.IHTTPSession session) {
+
+ String title = params.get("title");
+ if (title == null) {
+ return new RenderPair(null, Result.failure("Missing 'title'"));
+ }
+
+ APIProvider api = ApplicationContext.getInstance().getApi();
+
+ Result r = api.createForum(title);
+
+ if (r.isSuccess()) {
+ return new RenderPair("Success.ftl",
+ Result.success(new ValueHolder("Created new forum.")));
+ } else {
+ return new RenderPair(null, r);
+ }
+ }
+}
diff --git a/src/uk/ac/bris/cs/databases/web/CreatePersonHandler.java b/src/uk/ac/bris/cs/databases/web/CreatePersonHandler.java
new file mode 100644
index 0000000..7fd024c
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/web/CreatePersonHandler.java
@@ -0,0 +1,41 @@
+package uk.ac.bris.cs.databases.web;
+
+import fi.iki.elonen.NanoHTTPD;
+import java.util.Map;
+import uk.ac.bris.cs.databases.api.APIProvider;
+import uk.ac.bris.cs.databases.api.Result;
+
+/**
+ * Create a new person.
+ * path: POST /createperson [name,username,stuid]
+ *
+ * @author csxdb
+ */
+public class CreatePersonHandler extends AbstractPostHandler {
+
+ @Override
+ public RenderPair handlePost(Map params,
+ NanoHTTPD.IHTTPSession session) {
+
+ String name = params.get("name");
+ if (name == null || name.equals("")) {
+ return new RenderPair(null, Result.failure("Missing or empty name."));
+ }
+ String username = params.get("username");
+ if (username == null || username.equals("")) {
+ return new RenderPair(null, Result.failure("Missing or empty username."));
+ }
+ String sid = params.get("stuid");
+
+ APIProvider api = ApplicationContext.getInstance().getApi();
+
+ Result r = api.addNewPerson(name, username, sid.equals("") ? null : sid);
+
+ if (r.isSuccess()) {
+ return new RenderPair("Success.ftl",
+ Result.success(new ValueHolder("Created new person.")));
+ } else {
+ return new RenderPair(null, r);
+ }
+ }
+}
diff --git a/src/uk/ac/bris/cs/databases/web/CreatePostHandler.java b/src/uk/ac/bris/cs/databases/web/CreatePostHandler.java
new file mode 100644
index 0000000..5b753b9
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/web/CreatePostHandler.java
@@ -0,0 +1,46 @@
+package uk.ac.bris.cs.databases.web;
+
+import fi.iki.elonen.NanoHTTPD;
+import java.util.Map;
+import uk.ac.bris.cs.databases.api.APIProvider;
+import uk.ac.bris.cs.databases.api.Result;
+
+/**
+ *
+ * @author csxdb
+ */
+public class CreatePostHandler extends AbstractPostHandler {
+
+ @Override
+ public RenderPair handlePost(Map params,
+ NanoHTTPD.IHTTPSession session) {
+
+ NanoHTTPD.CookieHandler h = session.getCookies();
+ String name = h.read("user");
+
+ if (name == null || name.equals("")) {
+ return new RenderPair(null, Result.failure("Missing 'name'"));
+ }
+
+ int topicId = Integer.parseInt(params.get("topic"));
+ if (topicId == 0) {
+ return new RenderPair(null, Result.failure("Got zero topic id."));
+ }
+
+ String text = params.get("text");
+ if (text == null || text.equals("")) {
+ return new RenderPair(null, Result.failure("Missing 'text'"));
+ }
+
+ APIProvider api = ApplicationContext.getInstance().getApi();
+ Result r = api.createPost(topicId, name, text);
+
+ if (!r.isSuccess()) {
+ return new RenderPair(null, Result.failure(
+ "Failed to create post - " + r.getMessage()));
+ }
+
+ return new RenderPair("Success.ftl",
+ Result.success(new ValueHolder("Created a new post.")));
+ }
+}
diff --git a/src/uk/ac/bris/cs/databases/web/CreateTopicHandler.java b/src/uk/ac/bris/cs/databases/web/CreateTopicHandler.java
new file mode 100644
index 0000000..ec78cd6
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/web/CreateTopicHandler.java
@@ -0,0 +1,51 @@
+package uk.ac.bris.cs.databases.web;
+
+import fi.iki.elonen.NanoHTTPD;
+import java.util.Map;
+import uk.ac.bris.cs.databases.api.APIProvider;
+import uk.ac.bris.cs.databases.api.Result;
+
+/**
+ *
+ * @author csxdb
+ */
+public class CreateTopicHandler extends AbstractPostHandler {
+
+ @Override
+ public RenderPair handlePost(Map params,
+ NanoHTTPD.IHTTPSession session) {
+
+ NanoHTTPD.CookieHandler h = session.getCookies();
+ String name = h.read("user");
+
+ if (name == null || name.equals("")) {
+ return new RenderPair(null, Result.failure("Missing 'name'"));
+ }
+
+ int forumId = Integer.parseInt(params.get("forum"));
+ if (forumId == 0) {
+ return new RenderPair(null, Result.failure("Got zero forum id."));
+ }
+
+ String title = params.get("title");
+ if (title == null || title.equals("")) {
+ return new RenderPair(null, Result.failure("Missing 'title'"));
+ }
+
+ String text = params.get("text");
+ if (text == null || text.equals("")) {
+ return new RenderPair(null, Result.failure("Missing 'text'"));
+ }
+
+ APIProvider api = ApplicationContext.getInstance().getApi();
+ Result r = api.createTopic(forumId, name, title, text);
+
+ if (!r.isSuccess()) {
+ return new RenderPair(null, Result.failure(
+ "Failed to create topic - " + r.getMessage()));
+ }
+
+ return new RenderPair("Success.ftl",
+ Result.success(new ValueHolder("Created a new topic.")));
+ }
+}
diff --git a/src/uk/ac/bris/cs/databases/web/ForumHandler.java b/src/uk/ac/bris/cs/databases/web/ForumHandler.java
new file mode 100644
index 0000000..6e2257b
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/web/ForumHandler.java
@@ -0,0 +1,23 @@
+package uk.ac.bris.cs.databases.web;
+
+import uk.ac.bris.cs.databases.api.APIProvider;
+import uk.ac.bris.cs.databases.api.ForumView;
+import uk.ac.bris.cs.databases.api.Result;
+
+/**
+ * Handler for the advanced view of a single forum.
+ * path: /forum2/$id
+ *
+ * @author csxdb
+ */
+public class ForumHandler extends SimpleHandler {
+
+ @Override
+ RenderPair simpleRender(String p) throws RenderException {
+ int id = Integer.parseInt(p);
+ APIProvider api = ApplicationContext.getInstance().getApi();
+ Result r = api.getForum(id);
+ return new RenderPair("ForumView.ftl", r);
+ }
+
+}
diff --git a/src/uk/ac/bris/cs/databases/web/ForumsHandler.java b/src/uk/ac/bris/cs/databases/web/ForumsHandler.java
new file mode 100644
index 0000000..dee466b
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/web/ForumsHandler.java
@@ -0,0 +1,24 @@
+package uk.ac.bris.cs.databases.web;
+
+import java.util.List;
+import uk.ac.bris.cs.databases.api.APIProvider;
+import uk.ac.bris.cs.databases.api.Result;
+import uk.ac.bris.cs.databases.api.ForumSummaryView;
+
+/**
+* The simplified forums handler (no topic info at all).
+ * path: /forums0
+ *
+ * @author csxdb
+ */
+public class ForumsHandler extends SimpleHandler {
+
+ @Override
+ RenderPair simpleRender(String p) throws RenderException {
+ APIProvider api = ApplicationContext.getInstance().getApi();
+ Result> r = api.getForums();
+ return new RenderPair("ForumsView.ftl", ListWrapper.wrap(r));
+ }
+
+ @Override boolean needsParameter() { return false; }
+}
diff --git a/src/uk/ac/bris/cs/databases/web/LoginHandler.java b/src/uk/ac/bris/cs/databases/web/LoginHandler.java
new file mode 100644
index 0000000..5737540
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/web/LoginHandler.java
@@ -0,0 +1,69 @@
+package uk.ac.bris.cs.databases.web;
+
+import fi.iki.elonen.NanoHTTPD;
+import fi.iki.elonen.router.RouterNanoHTTPD;
+import java.util.Map;
+import uk.ac.bris.cs.databases.api.APIProvider;
+import uk.ac.bris.cs.databases.api.Result;
+
+/**
+ *
+ * @author David
+ */
+public class LoginHandler extends AbstractHandler {
+
+ String username = "";
+
+ @Override
+ public View render(RouterNanoHTTPD.UriResource uriResource,
+ Map params,
+ NanoHTTPD.IHTTPSession session) {
+
+ System.out.println("[LoginHandler] render " + session.getUri());
+
+ String id = params.get("id");
+ String template;
+ Result data;
+
+ if (id == null || id.equals("")) {
+ username = "";
+ template = "Success.ftl";
+ data = Result.success(new ValueHolder("Logged out."));
+ } else {
+ APIProvider api = ApplicationContext.getInstance().getApi();
+ Result> r = api.getUsers();
+ if (!r.isSuccess()) {
+ template = null;
+ data = Result.fatal("API call failed.");
+ } else {
+ Map users = r.getValue();
+ String name = users.get(id);
+ if (name == null) {
+ template = null;
+ data = Result.failure("No such user");
+ } else {
+ username = id;
+ template = "Success.ftl";
+ data = Result.success(new ValueHolder("Logged in as " + name));
+ }
+ }
+ }
+
+ NanoHTTPD.CookieHandler h = session.getCookies();
+ if (username.equals("")) {
+ h.delete("user");
+ } else {
+ h.set("user", username + ";Path=/", 1);
+ }
+
+ if (data.isSuccess()) {
+ System.out.println("[SimpleHandler] rendering " + template);
+ return renderView(template, data.getValue(),
+ username.equals("") ? null : username);
+ } else if (data.isFatal()) {
+ return new View(500, "Fatal error - " + data.getMessage());
+ } else {
+ return new View(400, "Error - " + data.getMessage());
+ }
+ }
+}
diff --git a/src/uk/ac/bris/cs/databases/web/NewForumHandler.java b/src/uk/ac/bris/cs/databases/web/NewForumHandler.java
new file mode 100644
index 0000000..882a880
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/web/NewForumHandler.java
@@ -0,0 +1,18 @@
+package uk.ac.bris.cs.databases.web;
+
+import uk.ac.bris.cs.databases.api.Result;
+
+/**
+ *
+ * @author csxdb
+ */
+public class NewForumHandler extends SimpleHandler {
+
+ @Override
+ RenderPair simpleRender(String p) throws RenderException {
+ return new RenderPair("NewForumView.ftl", Result.success());
+ }
+
+ @Override boolean needsParameter() { return false; }
+
+}
diff --git a/src/uk/ac/bris/cs/databases/web/NewPersonHandler.java b/src/uk/ac/bris/cs/databases/web/NewPersonHandler.java
new file mode 100644
index 0000000..c360bb6
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/web/NewPersonHandler.java
@@ -0,0 +1,20 @@
+package uk.ac.bris.cs.databases.web;
+
+import uk.ac.bris.cs.databases.api.Result;
+
+/**
+ * Create a new person.
+ * path: GET /newperson
+ *
+ * @author csxdb
+ */
+public class NewPersonHandler extends SimpleHandler {
+
+ @Override
+ RenderPair simpleRender(String p) throws RenderException {
+
+ return new RenderPair("NewPersonView.ftl", Result.success());
+ }
+
+ @Override boolean needsParameter() { return false; }
+}
diff --git a/src/uk/ac/bris/cs/databases/web/NewPostHandler.java b/src/uk/ac/bris/cs/databases/web/NewPostHandler.java
new file mode 100644
index 0000000..5a6543c
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/web/NewPostHandler.java
@@ -0,0 +1,19 @@
+package uk.ac.bris.cs.databases.web;
+
+import java.util.HashMap;
+import java.util.Map;
+import uk.ac.bris.cs.databases.api.Result;
+
+/**
+ *
+ * @author csxdb
+ */
+public class NewPostHandler extends SimpleHandler {
+
+ @Override
+ RenderPair simpleRender(String p) throws RenderException {
+ Map data = new HashMap<>();
+ data.put("topic", p);
+ return new RenderPair("NewPostView.ftl", Result.success(data));
+ }
+}
diff --git a/src/uk/ac/bris/cs/databases/web/NewTopicHandler.java b/src/uk/ac/bris/cs/databases/web/NewTopicHandler.java
new file mode 100644
index 0000000..4d3e676
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/web/NewTopicHandler.java
@@ -0,0 +1,32 @@
+package uk.ac.bris.cs.databases.web;
+
+import fi.iki.elonen.NanoHTTPD;
+import java.util.HashMap;
+import java.util.Map;
+import uk.ac.bris.cs.databases.api.Result;
+
+/**
+ *
+ * @author csxdb
+ */
+public class NewTopicHandler extends RPHandler {
+
+ @Override
+ RenderPair doRender(String p,
+ NanoHTTPD.IHTTPSession session)
+ throws RenderException {
+
+ NanoHTTPD.CookieHandler h = session.getCookies();
+ String username = h.read("user");
+ if (username == null || username.equals("")) {
+ return new RenderPair(null, Result.failure(
+ "You must log in to create new topics. " +
+ "Select 'list of users' in the top bar."));
+ }
+
+ Map data = new HashMap<>();
+ data.put("forum", p);
+
+ return new RenderPair("NewTopic.ftl", Result.success(data));
+ }
+}
diff --git a/src/uk/ac/bris/cs/databases/web/PeopleHandler.java b/src/uk/ac/bris/cs/databases/web/PeopleHandler.java
new file mode 100644
index 0000000..6703f72
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/web/PeopleHandler.java
@@ -0,0 +1,64 @@
+package uk.ac.bris.cs.databases.web;
+
+import fi.iki.elonen.NanoHTTPD;
+import fi.iki.elonen.router.RouterNanoHTTPD;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import uk.ac.bris.cs.databases.api.APIProvider;
+import uk.ac.bris.cs.databases.api.Result;
+
+/**
+ *
+ * @author csxdb
+ */
+public class PeopleHandler extends AbstractHandler {
+
+ public class KV {
+ private final String k;
+ private final String v;
+
+ public KV(String k, String v) {
+ this.k = k;
+ this.v = v;
+ }
+
+ /**
+ * @return the k
+ */
+ public String getKey() {
+ return k;
+ }
+
+ /**
+ * @return the v
+ */
+ public String getValue() {
+ return v;
+ }
+ }
+
+ @Override
+ public View render(RouterNanoHTTPD.UriResource uriResource,
+ Map params,
+ NanoHTTPD.IHTTPSession session) {
+ APIProvider api = ApplicationContext.getInstance().getApi();
+ Result> r = api.getUsers();
+
+ if (r.isSuccess()) {
+ List people = new ArrayList<>(r.getValue().size());
+ for (Map.Entry entry : r.getValue().entrySet()) {
+ people.add(new KV(entry.getKey(), entry.getValue()));
+ }
+
+ NanoHTTPD.CookieHandler h = session.getCookies();
+ String user = h.read("user");
+
+ return renderView("PeopleView.ftl", wrap(people), user);
+ } else {
+ return new View(500, "Database error - " + r.getMessage());
+ }
+
+ }
+
+}
diff --git a/src/uk/ac/bris/cs/databases/web/PersonHandler.java b/src/uk/ac/bris/cs/databases/web/PersonHandler.java
new file mode 100644
index 0000000..e381e3d
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/web/PersonHandler.java
@@ -0,0 +1,19 @@
+package uk.ac.bris.cs.databases.web;
+
+import uk.ac.bris.cs.databases.api.APIProvider;
+import uk.ac.bris.cs.databases.api.PersonView;
+import uk.ac.bris.cs.databases.api.Result;
+
+/**
+ * PATH /person/:id
+ * @author csxdb
+ */
+public class PersonHandler extends SimpleHandler {
+
+ @Override
+ RenderPair simpleRender(String p) throws RenderException {
+ APIProvider api = ApplicationContext.getInstance().getApi();
+ Result r = api.getPersonView(p);
+ return new RenderPair("PersonView.ftl", r);
+ }
+}
diff --git a/src/uk/ac/bris/cs/databases/web/RPHandler.java b/src/uk/ac/bris/cs/databases/web/RPHandler.java
new file mode 100644
index 0000000..2100fbf
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/web/RPHandler.java
@@ -0,0 +1,78 @@
+package uk.ac.bris.cs.databases.web;
+
+import fi.iki.elonen.NanoHTTPD;
+import fi.iki.elonen.router.RouterNanoHTTPD;
+import java.util.Map;
+import uk.ac.bris.cs.databases.api.Result;
+
+/**
+ * Handler that allows the renderer access to the session.
+ * It also provides the RenderPair abstraction, used primarily in the
+ * SimpleHandler subclass.
+ * @author csxdb
+ */
+public abstract class RPHandler extends AbstractHandler {
+
+ public class RenderException extends Exception {
+ int code;
+
+ public RenderException(int code, String message) {
+ super(message);
+ this.code = code;
+ }
+
+ }
+
+ public class RenderPair {
+ final String template;
+ final Result data;
+
+ public RenderPair(String template, Result data) {
+ this.template = template;
+ this.data = data;
+ }
+ }
+
+ abstract RenderPair doRender(String p,
+ NanoHTTPD.IHTTPSession session) throws RenderException;
+
+ // override if you don't need one.
+ boolean needsParameter() { return true; }
+
+ @Override
+ public View render(RouterNanoHTTPD.UriResource uriResource,
+ Map params,
+ NanoHTTPD.IHTTPSession session) {
+
+ System.out.println("[RPHandler] render " + session.getUri());
+
+ // Get the id or complain.
+
+ String id = params.get("id");
+ if (needsParameter()) {
+ if (id == null || id.equals("")) {
+ return new View(404, "Missing parameter.");
+ }
+ }
+
+ try {
+ RenderPair rp = doRender(id, session);
+
+ NanoHTTPD.CookieHandler h = session.getCookies();
+ String user = h.read("user");
+
+ if (rp.data.isSuccess()) {
+ System.out.println("[SimpleHandler] rendering " + rp.template);
+ return renderView(rp.template, rp.data.getValue(), user);
+ } else if (rp.data.isFatal()) {
+ return new View(500, "Fatal error - " + rp.data.getMessage());
+ } else {
+ return new View(400, "Error - " + rp.data.getMessage());
+ }
+
+ } catch (RenderException e) {
+ return new View(e.code, e.getMessage());
+ }
+
+ }
+}
diff --git a/src/uk/ac/bris/cs/databases/web/Server.java b/src/uk/ac/bris/cs/databases/web/Server.java
new file mode 100644
index 0000000..c907e29
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/web/Server.java
@@ -0,0 +1,99 @@
+/*
+ * Mini implementation forum server and UI.
+ */
+package uk.ac.bris.cs.databases.web;
+
+import fi.iki.elonen.router.RouterNanoHTTPD;
+import fi.iki.elonen.util.ServerRunner;
+import freemarker.template.Configuration;
+import java.io.File;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import uk.ac.bris.cs.databases.api.APIProvider;
+import uk.ac.bris.cs.databases.cwk2.API;
+
+/**
+ * @author csxdb
+ */
+public class Server extends RouterNanoHTTPD {
+
+ private static final String DATABASE = "jdbc:mariadb://localhost:3306/bb?user=student";
+
+ public Server() {
+ super(8000);
+ addMappings();
+ }
+
+ @Override public void addMappings() {
+ super.addMappings();
+ addRoute("/person/:id", PersonHandler.class);
+ addRoute("/people", PeopleHandler.class);
+ addRoute("/newtopic", NewTopicHandler.class);
+ addRoute("/forums", ForumsHandler.class);
+ addRoute("/forum/:id", ForumHandler.class);
+ addRoute("/topic/:id", TopicHandler.class);
+
+ addRoute("/newforum", NewForumHandler.class);
+ addRoute("/createforum", CreateForumHandler.class);
+
+ addRoute("/newtopic/:id", NewTopicHandler.class);
+ addRoute("/createtopic", CreateTopicHandler.class);
+
+ addRoute("/newpost/:id", NewPostHandler.class);
+ addRoute("/createpost", CreatePostHandler.class);
+
+ addRoute("/newperson", NewPersonHandler.class);
+ addRoute("/createperson", CreatePersonHandler.class);
+
+ addRoute("/login", LoginHandler.class);
+ addRoute("/login/:id", LoginHandler.class);
+
+ addRoute("/styles.css", StyleHandler.class, "resources/styles.css");
+ addRoute("/gridlex.css", StyleHandler.class, "resources/gridlex.css");
+ }
+
+ public static void main(String[] args) throws Exception {
+
+ ApplicationContext c = ApplicationContext.getInstance();
+
+ // database //
+
+ Connection conn;
+ try {
+ String cs = DATABASE;
+ if (args.length >= 1) {
+ cs = cs + "&localSocket=" + args[0];
+ System.out.println("Using DB socket file: " + args[0]);
+ } else {
+ //System.out.println("Not using a DB socket file.");
+ }
+
+ conn = DriverManager.getConnection(cs);
+ conn.setAutoCommit(false);
+ APIProvider api = new API(conn);
+ c.setApi(api);
+
+ // AF: Info messages
+ System.out.println("Server accessible at: http://localhost:8000");
+ System.out.println("Forums accessible at: http://localhost:8000/forums");
+ } catch (SQLException e) {
+ System.out.println("Connection to database failed. " +
+ "Check that the database is running and that the socket file " +
+ "is correct if you are using one.");
+ throw new RuntimeException(e);
+ }
+
+ // templating //
+
+ Configuration cfg = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
+ cfg.setDirectoryForTemplateLoading(new File("resources/templates"));
+ cfg.setDefaultEncoding("UTF-8");
+ c.setTemplateConfiguration(cfg);
+
+ // server //
+
+ Server server = new Server();
+ ServerRunner.run(Server.class);
+ }
+}
diff --git a/src/uk/ac/bris/cs/databases/web/SimpleHandler.java b/src/uk/ac/bris/cs/databases/web/SimpleHandler.java
new file mode 100644
index 0000000..147994c
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/web/SimpleHandler.java
@@ -0,0 +1,19 @@
+package uk.ac.bris.cs.databases.web;
+
+import fi.iki.elonen.NanoHTTPD;
+
+/**
+ * Handler for the simple case that we're dealing with a GET request, have a
+ * single URI parameter of interest and want to render a view with some data
+ * of complain if we get junk back.
+ * @author csxdb
+ */
+public abstract class SimpleHandler extends RPHandler {
+
+ abstract RenderPair simpleRender(String p) throws RenderException;
+
+ @Override public RenderPair doRender(String p,
+ NanoHTTPD.IHTTPSession session) throws RenderException {
+ return simpleRender(p);
+ }
+}
diff --git a/src/uk/ac/bris/cs/databases/web/StyleHandler.java b/src/uk/ac/bris/cs/databases/web/StyleHandler.java
new file mode 100644
index 0000000..5fbcd7f
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/web/StyleHandler.java
@@ -0,0 +1,48 @@
+package uk.ac.bris.cs.databases.web;
+
+import fi.iki.elonen.NanoHTTPD;
+import fi.iki.elonen.router.RouterNanoHTTPD;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ *
+ * @author David
+ */
+public class StyleHandler extends AbstractHandler {
+
+ @Override
+ public View render(RouterNanoHTTPD.UriResource uriResource,
+ Map params,
+ NanoHTTPD.IHTTPSession session) {
+
+ String filename = uriResource.initParameter(String.class);
+
+ try {
+ File f = new File(filename);
+ FileReader fr = new FileReader(f);
+ BufferedReader br = new BufferedReader(fr);
+
+ StringBuilder sb = new StringBuilder();
+ String line;
+ while ((line = br.readLine()) != null) {
+ sb.append(line);
+ sb.append("\n");
+ }
+ br.close();
+ return new View(200, sb.toString());
+ } catch (IOException e) {
+ return new View(500, "Error reading file - " + e.getMessage());
+ }
+ }
+
+ @Override
+ public String getMimeType() {
+ return "text/css";
+ }
+
+
+}
diff --git a/src/uk/ac/bris/cs/databases/web/TopicHandler.java b/src/uk/ac/bris/cs/databases/web/TopicHandler.java
new file mode 100644
index 0000000..a89ae46
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/web/TopicHandler.java
@@ -0,0 +1,21 @@
+package uk.ac.bris.cs.databases.web;
+
+import uk.ac.bris.cs.databases.api.APIProvider;
+import uk.ac.bris.cs.databases.api.Result;
+import uk.ac.bris.cs.databases.api.TopicView;
+
+/**
+ *
+ * @author csxdb
+ */
+public class TopicHandler extends SimpleHandler {
+
+ @Override
+ public RenderPair simpleRender(String p) {
+
+ int id = Integer.parseInt(p);
+ APIProvider api = ApplicationContext.getInstance().getApi();
+ Result r = api.getTopic(id);
+ return new RenderPair("TopicView.ftl", r);
+ }
+}
diff --git a/src/uk/ac/bris/cs/databases/web/View.java b/src/uk/ac/bris/cs/databases/web/View.java
new file mode 100644
index 0000000..8401c44
--- /dev/null
+++ b/src/uk/ac/bris/cs/databases/web/View.java
@@ -0,0 +1,46 @@
+package uk.ac.bris.cs.databases.web;
+
+/**
+ *
+ * @author csxdb
+ */
+public class View {
+ private int code;
+ private String contents;
+
+ public View() {
+ }
+
+ public View(int code, String contents) {
+ this.code = code;
+ this.contents = contents;
+ }
+
+ /**
+ * @return the code
+ */
+ public int getCode() {
+ return code;
+ }
+
+ /**
+ * @param code the code to set
+ */
+ public void setCode(int code) {
+ this.code = code;
+ }
+
+ /**
+ * @return the contents
+ */
+ public String getContents() {
+ return contents;
+ }
+
+ /**
+ * @param contents the contents to set
+ */
+ public void setContents(String contents) {
+ this.contents = contents;
+ }
+}
diff --git a/target/uk/ac/bris/cs/databases/api/APIProvider.class b/target/uk/ac/bris/cs/databases/api/APIProvider.class
new file mode 100644
index 0000000..c029d96
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/api/APIProvider.class differ
diff --git a/target/uk/ac/bris/cs/databases/api/ForumSummaryView.class b/target/uk/ac/bris/cs/databases/api/ForumSummaryView.class
new file mode 100644
index 0000000..12c5bad
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/api/ForumSummaryView.class differ
diff --git a/target/uk/ac/bris/cs/databases/api/ForumView.class b/target/uk/ac/bris/cs/databases/api/ForumView.class
new file mode 100644
index 0000000..05c9a4a
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/api/ForumView.class differ
diff --git a/target/uk/ac/bris/cs/databases/api/PersonView.class b/target/uk/ac/bris/cs/databases/api/PersonView.class
new file mode 100644
index 0000000..4897112
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/api/PersonView.class differ
diff --git a/target/uk/ac/bris/cs/databases/api/Result$1.class b/target/uk/ac/bris/cs/databases/api/Result$1.class
new file mode 100644
index 0000000..1acccd2
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/api/Result$1.class differ
diff --git a/target/uk/ac/bris/cs/databases/api/Result$Outcome.class b/target/uk/ac/bris/cs/databases/api/Result$Outcome.class
new file mode 100644
index 0000000..4ab3883
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/api/Result$Outcome.class differ
diff --git a/target/uk/ac/bris/cs/databases/api/Result.class b/target/uk/ac/bris/cs/databases/api/Result.class
new file mode 100644
index 0000000..38d52f4
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/api/Result.class differ
diff --git a/target/uk/ac/bris/cs/databases/api/SimplePostView.class b/target/uk/ac/bris/cs/databases/api/SimplePostView.class
new file mode 100644
index 0000000..718d1ef
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/api/SimplePostView.class differ
diff --git a/target/uk/ac/bris/cs/databases/api/SimpleTopicSummaryView.class b/target/uk/ac/bris/cs/databases/api/SimpleTopicSummaryView.class
new file mode 100644
index 0000000..0f92d26
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/api/SimpleTopicSummaryView.class differ
diff --git a/target/uk/ac/bris/cs/databases/api/TopicView.class b/target/uk/ac/bris/cs/databases/api/TopicView.class
new file mode 100644
index 0000000..ea77a0e
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/api/TopicView.class differ
diff --git a/target/uk/ac/bris/cs/databases/cwk2/API.class b/target/uk/ac/bris/cs/databases/cwk2/API.class
new file mode 100644
index 0000000..5fb1172
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/cwk2/API.class differ
diff --git a/target/uk/ac/bris/cs/databases/util/ParameterCannotBeEmptyException.class b/target/uk/ac/bris/cs/databases/util/ParameterCannotBeEmptyException.class
new file mode 100644
index 0000000..c6a8c56
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/util/ParameterCannotBeEmptyException.class differ
diff --git a/target/uk/ac/bris/cs/databases/util/ParameterCannotBeNullException.class b/target/uk/ac/bris/cs/databases/util/ParameterCannotBeNullException.class
new file mode 100644
index 0000000..878900f
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/util/ParameterCannotBeNullException.class differ
diff --git a/target/uk/ac/bris/cs/databases/util/ParameterException.class b/target/uk/ac/bris/cs/databases/util/ParameterException.class
new file mode 100644
index 0000000..2347042
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/util/ParameterException.class differ
diff --git a/target/uk/ac/bris/cs/databases/util/Params.class b/target/uk/ac/bris/cs/databases/util/Params.class
new file mode 100644
index 0000000..3c7597d
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/util/Params.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/AbstractHandler$ListWrapper.class b/target/uk/ac/bris/cs/databases/web/AbstractHandler$ListWrapper.class
new file mode 100644
index 0000000..d62b12c
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/AbstractHandler$ListWrapper.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/AbstractHandler$Status.class b/target/uk/ac/bris/cs/databases/web/AbstractHandler$Status.class
new file mode 100644
index 0000000..3143967
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/AbstractHandler$Status.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/AbstractHandler$ValueHolder.class b/target/uk/ac/bris/cs/databases/web/AbstractHandler$ValueHolder.class
new file mode 100644
index 0000000..d5dcaef
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/AbstractHandler$ValueHolder.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/AbstractHandler.class b/target/uk/ac/bris/cs/databases/web/AbstractHandler.class
new file mode 100644
index 0000000..277c048
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/AbstractHandler.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/AbstractPostHandler$RenderPair.class b/target/uk/ac/bris/cs/databases/web/AbstractPostHandler$RenderPair.class
new file mode 100644
index 0000000..038e49c
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/AbstractPostHandler$RenderPair.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/AbstractPostHandler$ValueHolder.class b/target/uk/ac/bris/cs/databases/web/AbstractPostHandler$ValueHolder.class
new file mode 100644
index 0000000..6ac4ea4
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/AbstractPostHandler$ValueHolder.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/AbstractPostHandler.class b/target/uk/ac/bris/cs/databases/web/AbstractPostHandler.class
new file mode 100644
index 0000000..91b7902
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/AbstractPostHandler.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/ApplicationContext.class b/target/uk/ac/bris/cs/databases/web/ApplicationContext.class
new file mode 100644
index 0000000..51659be
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/ApplicationContext.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/CreateForumHandler.class b/target/uk/ac/bris/cs/databases/web/CreateForumHandler.class
new file mode 100644
index 0000000..d75c6a9
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/CreateForumHandler.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/CreatePersonHandler.class b/target/uk/ac/bris/cs/databases/web/CreatePersonHandler.class
new file mode 100644
index 0000000..3a5bf86
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/CreatePersonHandler.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/CreatePostHandler.class b/target/uk/ac/bris/cs/databases/web/CreatePostHandler.class
new file mode 100644
index 0000000..ab0a538
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/CreatePostHandler.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/CreateTopicHandler.class b/target/uk/ac/bris/cs/databases/web/CreateTopicHandler.class
new file mode 100644
index 0000000..bbffb3a
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/CreateTopicHandler.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/ForumHandler.class b/target/uk/ac/bris/cs/databases/web/ForumHandler.class
new file mode 100644
index 0000000..3f903ed
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/ForumHandler.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/ForumsHandler.class b/target/uk/ac/bris/cs/databases/web/ForumsHandler.class
new file mode 100644
index 0000000..9f9b5a9
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/ForumsHandler.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/LoginHandler.class b/target/uk/ac/bris/cs/databases/web/LoginHandler.class
new file mode 100644
index 0000000..0fafd37
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/LoginHandler.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/NewForumHandler.class b/target/uk/ac/bris/cs/databases/web/NewForumHandler.class
new file mode 100644
index 0000000..5337300
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/NewForumHandler.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/NewPersonHandler.class b/target/uk/ac/bris/cs/databases/web/NewPersonHandler.class
new file mode 100644
index 0000000..b5b04ea
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/NewPersonHandler.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/NewPostHandler.class b/target/uk/ac/bris/cs/databases/web/NewPostHandler.class
new file mode 100644
index 0000000..0265eff
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/NewPostHandler.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/NewTopicHandler.class b/target/uk/ac/bris/cs/databases/web/NewTopicHandler.class
new file mode 100644
index 0000000..62eedc8
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/NewTopicHandler.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/PeopleHandler$KV.class b/target/uk/ac/bris/cs/databases/web/PeopleHandler$KV.class
new file mode 100644
index 0000000..7d5b569
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/PeopleHandler$KV.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/PeopleHandler.class b/target/uk/ac/bris/cs/databases/web/PeopleHandler.class
new file mode 100644
index 0000000..2dd0280
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/PeopleHandler.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/PersonHandler.class b/target/uk/ac/bris/cs/databases/web/PersonHandler.class
new file mode 100644
index 0000000..062b471
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/PersonHandler.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/RPHandler$RenderException.class b/target/uk/ac/bris/cs/databases/web/RPHandler$RenderException.class
new file mode 100644
index 0000000..ac64a72
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/RPHandler$RenderException.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/RPHandler$RenderPair.class b/target/uk/ac/bris/cs/databases/web/RPHandler$RenderPair.class
new file mode 100644
index 0000000..ff673e7
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/RPHandler$RenderPair.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/RPHandler.class b/target/uk/ac/bris/cs/databases/web/RPHandler.class
new file mode 100644
index 0000000..11a812f
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/RPHandler.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/Server.class b/target/uk/ac/bris/cs/databases/web/Server.class
new file mode 100644
index 0000000..aec43c3
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/Server.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/SimpleHandler.class b/target/uk/ac/bris/cs/databases/web/SimpleHandler.class
new file mode 100644
index 0000000..3eb00a5
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/SimpleHandler.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/StyleHandler.class b/target/uk/ac/bris/cs/databases/web/StyleHandler.class
new file mode 100644
index 0000000..75f5410
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/StyleHandler.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/TopicHandler.class b/target/uk/ac/bris/cs/databases/web/TopicHandler.class
new file mode 100644
index 0000000..ac977a6
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/TopicHandler.class differ
diff --git a/target/uk/ac/bris/cs/databases/web/View.class b/target/uk/ac/bris/cs/databases/web/View.class
new file mode 100644
index 0000000..3aedf85
Binary files /dev/null and b/target/uk/ac/bris/cs/databases/web/View.class differ
+
+
diff --git a/resources/templates/header.html b/resources/templates/header.html
new file mode 100644
index 0000000..0c4ef0e
--- /dev/null
+++ b/resources/templates/header.html
@@ -0,0 +1,19 @@
+
+