Browse Source

OfficeFloor implementation of Framework Benchmark Performance Tests.

sagenschneider 9 years ago
parent
commit
33d7828efd
33 changed files with 1690 additions and 0 deletions
  1. 1 0
      frameworks/Java/officefloor/.gitignore
  2. 28 0
      frameworks/Java/officefloor/benchmark_config.json
  3. 119 0
      frameworks/Java/officefloor/pom.xml
  4. 5 0
      frameworks/Java/officefloor/raw/datasource.properties
  5. 42 0
      frameworks/Java/officefloor/setup.sh
  6. 20 0
      frameworks/Java/officefloor/source_code
  7. 28 0
      frameworks/Java/officefloor/src/main/java/net/officefloor/performance/entities/Fortune.java
  8. 19 0
      frameworks/Java/officefloor/src/main/java/net/officefloor/performance/entities/World.java
  9. 59 0
      frameworks/Java/officefloor/src/main/java/net/officefloor/performance/logic/DatabaseUpdateLogic.java
  10. 54 0
      frameworks/Java/officefloor/src/main/java/net/officefloor/performance/logic/FortunesLogic.java
  11. 24 0
      frameworks/Java/officefloor/src/main/java/net/officefloor/performance/logic/JsonSerialisationLogic.java
  12. 57 0
      frameworks/Java/officefloor/src/main/java/net/officefloor/performance/logic/LogicUtil.java
  13. 43 0
      frameworks/Java/officefloor/src/main/java/net/officefloor/performance/logic/MultipleDatabaseQueriesLogic.java
  14. 30 0
      frameworks/Java/officefloor/src/main/java/net/officefloor/performance/logic/SingleDatabaseQueryLogic.java
  15. 18 0
      frameworks/Java/officefloor/src/main/resources/META-INF/persistence.xml
  16. 9 0
      frameworks/Java/officefloor/src/main/resources/application.objects
  17. 5 0
      frameworks/Java/officefloor/src/main/resources/application.teams
  18. 26 0
      frameworks/Java/officefloor/src/main/resources/application.woof
  19. 7 0
      frameworks/Java/officefloor/src/main/resources/c3p0.properties
  20. 5 0
      frameworks/Java/officefloor/src/main/resources/datasource.properties
  21. 11 0
      frameworks/Java/officefloor/src/main/resources/log4j.properties
  22. 7 0
      frameworks/Java/officefloor/src/main/webapp/WEB-INF/web.xml
  23. 11 0
      frameworks/Java/officefloor/src/main/webapp/fortune.woof.html
  24. 1 0
      frameworks/Java/officefloor/src/main/webapp/plaintext.woof.html
  25. 5 0
      frameworks/Java/officefloor/src/main/webapp/service.woof.html
  26. 256 0
      frameworks/Java/officefloor/src/test/java/net/officefloor/performance/AbstractDatabaseQueryTestCase.java
  27. 372 0
      frameworks/Java/officefloor/src/test/java/net/officefloor/performance/AbstractTestCase.java
  28. 113 0
      frameworks/Java/officefloor/src/test/java/net/officefloor/performance/DatabaseUpdateTest.java
  29. 155 0
      frameworks/Java/officefloor/src/test/java/net/officefloor/performance/FortunesTest.java
  30. 30 0
      frameworks/Java/officefloor/src/test/java/net/officefloor/performance/JsonSerialisationTest.java
  31. 61 0
      frameworks/Java/officefloor/src/test/java/net/officefloor/performance/MultipleDatabaseQueriesTest.java
  32. 28 0
      frameworks/Java/officefloor/src/test/java/net/officefloor/performance/PlainTextTest.java
  33. 41 0
      frameworks/Java/officefloor/src/test/java/net/officefloor/performance/SingleDatabaseQueryTest.java

+ 1 - 0
frameworks/Java/officefloor/.gitignore

@@ -0,0 +1 @@
+/production/

+ 28 - 0
frameworks/Java/officefloor/benchmark_config.json

@@ -0,0 +1,28 @@
+{
+  "framework": "officefloor",
+  "tests": [{
+    "default": {
+      "setup_file": "setup",
+      "json_url": "/json-service.woof",
+      "db_url": "/singleQuery-service.woof",
+      "query_url": "/multipleQueries-service.woof?queries=",
+      "fortune_url": "/fortune.woof",
+      "update_url": "/databaseUpdate-service.woof?queries=",
+      "plaintext_url": "/plaintext.woof",
+      "port": 7878,
+      "approach": "Realistic",
+      "classification": "Fullstack",
+      "database": "MySQL",
+      "framework": "officefloor",
+      "language": "Java",
+      "orm": "Full",
+      "platform": "OfficeFloor",
+      "webserver": "WoOF (Web on OfficeFloor)",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "officefloor",
+      "notes": "",
+      "versus": ""
+    }
+  }]
+}

+ 119 - 0
frameworks/Java/officefloor/pom.xml

@@ -0,0 +1,119 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<groupId>net.officefloor.performance</groupId>
+	<artifactId>TechEmpowerPerformance</artifactId>
+	<version>0.0.1</version>
+	<packaging>war</packaging>
+	<name>TechEmpowerPerformance</name>
+	<properties>
+		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+		<officefloor-version>2.16.0</officefloor-version>
+		<compiler-version>3.1</compiler-version>
+		<datanucleus-version>4.0.0-release</datanucleus-version>
+		<mysql-version>5.1.36</mysql-version>
+		<junit-version>4.11</junit-version>
+	</properties>
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-war-plugin</artifactId>
+				<version>2.4</version>
+			</plugin>
+			<plugin>
+				<!-- Standard JVM for compilation -->
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<version>${compiler-version}</version>
+				<configuration>
+					<source>1.8</source>
+					<target>1.8</target>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>org.datanucleus</groupId>
+				<artifactId>datanucleus-maven-plugin</artifactId>
+				<version>${datanucleus-version}</version>
+				<configuration>
+					<api>JPA</api>
+					<verbose>true</verbose>
+				</configuration>
+				<executions>
+					<execution>
+						<phase>process-classes</phase>
+						<goals>
+							<goal>enhance</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+		</plugins>
+	</build>
+	<dependencies>
+		<dependency>
+			<groupId>net.officefloor.plugin</groupId>
+			<artifactId>officeplugin_woof</artifactId>
+			<version>${officefloor-version}</version>
+		</dependency>
+		<dependency>
+			<groupId>net.officefloor.plugin</groupId>
+			<artifactId>officeplugin_json</artifactId>
+			<version>${officefloor-version}</version>
+		</dependency>
+		<dependency>
+			<groupId>net.officefloor.plugin</groupId>
+			<artifactId>officeplugin_jpa</artifactId>
+			<version>${officefloor-version}</version>
+		</dependency>
+		<dependency>
+			<groupId>log4j</groupId>
+			<artifactId>log4j</artifactId>
+			<version>1.2.17</version>
+		</dependency>
+		<dependency>
+			<groupId>org.eclipse.persistence</groupId>
+			<artifactId>javax.persistence</artifactId>
+			<version>2.1.0</version>
+		</dependency>
+		<dependency>
+			<groupId>org.datanucleus</groupId>
+			<artifactId>datanucleus-accessplatform-jpa-rdbms</artifactId>
+			<type>pom</type>
+			<version>${datanucleus-version}</version>
+		</dependency>
+		<dependency>
+			<groupId>mysql</groupId>
+			<artifactId>mysql-connector-java</artifactId>
+			<version>${mysql-version}</version>
+		</dependency>
+		<dependency>
+			<groupId>com.mchange</groupId>
+			<artifactId>c3p0</artifactId>
+			<version>0.9.5.1</version>
+		</dependency>
+		<dependency>
+			<groupId>org.projectlombok</groupId>
+			<artifactId>lombok</artifactId>
+			<version>1.12.4</version>
+		</dependency>
+		<dependency>
+			<!-- Should be scope test, but available to run stand alone for manual testing -->
+			<groupId>com.h2database</groupId>
+			<artifactId>h2</artifactId>
+			<version>1.4.187</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.httpcomponents</groupId>
+			<artifactId>httpclient</artifactId>
+			<version>4.3.2</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>junit</groupId>
+			<artifactId>junit</artifactId>
+			<version>${junit-version}</version>
+			<scope>test</scope>
+		</dependency>
+	</dependencies>
+</project>

+ 5 - 0
frameworks/Java/officefloor/raw/datasource.properties

@@ -0,0 +1,5 @@
+datanucleus.ConnectionDriverName = com.mysql.jdbc.Driver
+datanucleus.ConnectionURL = jdbc:mysql://DATABASE_HOST:3306/hello_world?jdbcCompliantTruncation=false&elideSetAutoCommits=true&useLocalSessionState=true&cachePrepStmts=true&cacheCallableStmts=true&alwaysSendSetIsolation=false&prepStmtCacheSize=4096&cacheServerConfiguration=true&prepStmtCacheSqlLimit=2048&zeroDateTimeBehavior=convertToNull&traceProtocol=false&useUnbufferedInput=false&useReadAheadInput=false&maintainTimeStats=false&useServerPrepStmts&cacheRSMetadata=true
+datanucleus.ConnectionUserName = benchmarkdbuser
+datanucleus.ConnectionPassword = benchmarkdbpass
+datanucleus.connectionPoolingType = C3P0

+ 42 - 0
frameworks/Java/officefloor/setup.sh

@@ -0,0 +1,42 @@
+#!/bin/bash
+
+# Propagate any failure
+set -e
+
+# Install Java8 manually (fw_depends java8 is failing in vagrant-development)
+sudo add-apt-repository -y ppa:webupd8team/java
+sudo apt-get update
+echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | sudo /usr/bin/debconf-set-selections
+sudo apt-get install -y oracle-java8-installer
+
+# Ensure maven uses appropriate version of Java
+export JAVA_HOME=/usr/lib/jvm/java-8-oracle/
+sudo update-alternatives --set java /usr/lib/jvm/java-8-oracle/jre/bin/java
+sudo update-alternatives --set javac /usr/lib/jvm/java-8-oracle/bin/javac
+
+# Need maven 3.1.1 or higher (default for Ubuntu is 3.0.5)
+echo "Loading maven ..."
+sudo add-apt-repository "deb http://ppa.launchpad.net/natecarlson/maven3/ubuntu precise main"
+sudo apt-get update
+sudo apt-get -y --force-yes install maven3
+if [ -e /usr/bin/mvn ]
+then
+    sudo rm -f /usr/bin/mvn
+fi
+sudo ln -s /usr/share/maven3/bin/mvn /usr/bin/mvn
+
+# Setup configuration file (normally properties files contains environment specific information but create copy to avoid SCM issues)
+echo "Creating configuration for OfficeFloor environment ..."
+mkdir -p ./production
+cp ./raw/datasource.properties ./production 
+sed -i 's|DATABASE_HOST|'"${DBHOST}"'|g' ./production/datasource.properties
+echo "Configuration created"
+
+# Compile application
+echo "Building OfficeFloor test application ..."
+mvn -DskipTests clean package
+echo "OfficeFloor test application built"
+
+# Run application
+echo "Starting OfficeFloor application"
+mvn -DincludeGWT=false -DenvDir=production net.officefloor.maven:woof-maven-plugin:run

+ 20 - 0
frameworks/Java/officefloor/source_code

@@ -0,0 +1,20 @@
+./officefloor/pom.xml
+./officefloor/src/main/java/net/officefloor/performance/entities/Fortune.java
+./officefloor/src/main/java/net/officefloor/performance/entities/World.java
+./officefloor/src/main/java/net/officefloor/performance/logic/DatabaseUpdateLogic.java
+./officefloor/src/main/java/net/officefloor/performance/logic/FortunesLogic.java
+./officefloor/src/main/java/net/officefloor/performance/logic/JsonSerialisationLogic.java
+./officefloor/src/main/java/net/officefloor/performance/logic/LogicUtil.java
+./officefloor/src/main/java/net/officefloor/performance/logic/MultipleDatabaseQueriesLogic.java
+./officefloor/src/main/java/net/officefloor/performance/logic/SingleDatabaseQueryLogic.java
+./officefloor/src/main/resources/application.objects
+./officefloor/src/main/resources/application.teams
+./officefloor/src/main/resources/application.woof
+./officefloor/src/main/resources/c3p0.properties
+./officefloor/src/main/resources/datasource.properties
+./officefloor/src/main/resources/log4j.properties
+./officefloor/src/main/resources/META-INF/persistence.xml
+./officefloor/src/main/webapp/fortune.woof.html
+./officefloor/src/main/webapp/plaintext.woof.html
+./officefloor/src/main/webapp/service.woof.html
+./officefloor/src/main/webapp/WEB-INF/web.xml

+ 28 - 0
frameworks/Java/officefloor/src/main/java/net/officefloor/performance/entities/Fortune.java

@@ -0,0 +1,28 @@
+package net.officefloor.performance.entities;
+
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+
+import net.officefloor.plugin.web.http.template.UnescapedHtml;
+import lombok.Data;
+import lombok.NonNull;
+
+/**
+ * Fortune entity.
+ * 
+ * @author Daniel Sagenschneider
+ */
+@Data
+@Entity
+@NamedQueries({ @NamedQuery(name = "Fortune.getAll", query = "SELECT e FROM Fortune e") })
+public class Fortune {
+	public static final String NAMED_QUERY_ALL = "Fortune.getAll";
+
+	@Id
+	private int id;
+	
+	@NonNull
+	private String message;
+}

+ 19 - 0
frameworks/Java/officefloor/src/main/java/net/officefloor/performance/entities/World.java

@@ -0,0 +1,19 @@
+package net.officefloor.performance.entities;
+
+import javax.persistence.Entity;
+import javax.persistence.Id;
+
+import lombok.Data;
+
+/**
+ * World entity.
+ * 
+ * @author Daniel Sagenschneider
+ */
+@Data
+@Entity
+public class World {
+	@Id
+	private int id;
+	private int randomNumber;
+}

+ 59 - 0
frameworks/Java/officefloor/src/main/java/net/officefloor/performance/logic/DatabaseUpdateLogic.java

@@ -0,0 +1,59 @@
+package net.officefloor.performance.logic;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+
+import javax.persistence.EntityManager;
+
+import lombok.Data;
+import net.officefloor.performance.entities.World;
+import net.officefloor.plugin.json.JsonResponseWriter;
+import net.officefloor.plugin.web.http.application.HttpParameters;
+
+/**
+ * Logic for Database Update query.
+ * 
+ * @author Daniel Sagenschneider
+ */
+public class DatabaseUpdateLogic {
+
+	@Data
+	@HttpParameters
+	public static class Parameters implements Serializable {
+		private String queries;
+	}
+
+	public void service(Parameters parameters, EntityManager entityManager,
+			JsonResponseWriter response) throws IOException {
+
+		// Obtain the number of queries
+		int queryCount = LogicUtil.getQueryCount(parameters.queries);
+
+		// Create the listing of random identifiers
+		int[] identifiers = new int[queryCount];
+		for (int i = 0; i < identifiers.length; i++) {
+			identifiers[i] = LogicUtil.generateRandomNumber(1, 10000);
+		}
+
+		// Sort identifiers to avoid dead locks
+		Arrays.sort(identifiers);
+
+		// Obtain the world objects (changing their random values)
+		World[] list = new World[queryCount];
+		for (int i = 0; i < list.length; i++) {
+
+			// Obtain the object
+			int identifier = identifiers[i];
+			World world = entityManager.find(World.class, identifier);
+			list[i] = world;
+
+			// Change the random value
+			world.setRandomNumber(LogicUtil.generateRandomNumber(1, 10000));
+		}
+
+		// Send the response
+		response.writeResponse(list);
+	}
+
+}

+ 54 - 0
frameworks/Java/officefloor/src/main/java/net/officefloor/performance/logic/FortunesLogic.java

@@ -0,0 +1,54 @@
+package net.officefloor.performance.logic;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+
+import javax.persistence.EntityManager;
+
+import lombok.Data;
+import net.officefloor.performance.entities.Fortune;
+
+/**
+ * Logic for Database Update query.
+ * 
+ * @author Daniel Sagenschneider
+ */
+public class FortunesLogic {
+
+	@Data
+	public static class TemplateData {
+		private final Fortune[] fortunes;
+	}
+
+	public TemplateData getTemplate(EntityManager entityManager) {
+
+		// Obtain all the fortunes
+		List<Fortune> list = entityManager.createNamedQuery(
+				Fortune.NAMED_QUERY_ALL, Fortune.class).getResultList();
+
+		// Obtain the fortunes as array
+		Fortune[] fortunes = list.toArray(new Fortune[list.size() + 1]);
+		int index = 0;
+		for (Fortune fortune : list) {
+			fortunes[index++] = fortune;
+		}
+
+		// Add the necessary Fortune
+		fortunes[fortunes.length - 1] = new Fortune(
+				"Additional fortune added at request time.");
+
+		// Sort the fortunes
+		Arrays.sort(fortunes, new Comparator<Fortune>() {
+			@Override
+			public int compare(Fortune a, Fortune b) {
+				return String.CASE_INSENSITIVE_ORDER.compare(a.getMessage(),
+						b.getMessage());
+			}
+		});
+
+		// Return the data for the template
+		return new TemplateData(fortunes);
+	}
+
+}

+ 24 - 0
frameworks/Java/officefloor/src/main/java/net/officefloor/performance/logic/JsonSerialisationLogic.java

@@ -0,0 +1,24 @@
+package net.officefloor.performance.logic;
+
+import java.io.IOException;
+
+import lombok.Data;
+import net.officefloor.plugin.json.JsonResponseWriter;
+
+/**
+ * Logic for JSON Serialisation.
+ * 
+ * @author Daniel Sagenschneider
+ */
+public class JsonSerialisationLogic {
+
+	@Data
+	public static class Message {
+		private final String message;
+	}
+
+	public void service(JsonResponseWriter writer) throws IOException {
+		writer.writeResponse(new Message("Hello, World!"));
+	}
+
+}

+ 57 - 0
frameworks/Java/officefloor/src/main/java/net/officefloor/performance/logic/LogicUtil.java

@@ -0,0 +1,57 @@
+package net.officefloor.performance.logic;
+
+/**
+ * Utility methods.
+ * 
+ * @author Daniel Sagenschneider
+ */
+public class LogicUtil {
+
+	/**
+	 * All access via static methods.
+	 */
+	private LogicUtil() {
+	}
+
+	/**
+	 * Generates the random number within the range.
+	 * 
+	 * @return Random number within the range.
+	 */
+	public static int generateRandomNumber(int start, int end) {
+		double randomValue = Math.random();
+		int randomNumber = (int) (start + Math.round(randomValue
+				* (end - start)));
+		return randomNumber;
+	}
+
+	/**
+	 * Obtains the query count.
+	 * 
+	 * @param queries
+	 *            Parameter value for <code>queries</code>.
+	 * @return Query count.
+	 */
+	public static int getQueryCount(String queries) {
+
+		// Obtain the number of queries to make
+		int queryCount;
+		try {
+			queryCount = Integer.parseInt(queries);
+		} catch (NumberFormatException ex) {
+			return 1;
+		} catch (NullPointerException ex) {
+			return 1;
+		}
+
+		// Ensure within bounds
+		if (queryCount < 1) {
+			return 1;
+		} else if (queryCount > 500) {
+			return 500;
+		} else {
+			return queryCount;
+		}
+	}
+
+}

+ 43 - 0
frameworks/Java/officefloor/src/main/java/net/officefloor/performance/logic/MultipleDatabaseQueriesLogic.java

@@ -0,0 +1,43 @@
+package net.officefloor.performance.logic;
+
+import java.io.IOException;
+import java.io.Serializable;
+
+import javax.persistence.EntityManager;
+
+import lombok.Data;
+import net.officefloor.performance.entities.World;
+import net.officefloor.plugin.json.JsonResponseWriter;
+import net.officefloor.plugin.web.http.application.HttpParameters;
+
+/**
+ * Logic for Multiple Database Queries.
+ * 
+ * @author Daniel Sagenschneider
+ */
+public class MultipleDatabaseQueriesLogic {
+
+	@Data
+	@HttpParameters
+	public static class Parameters implements Serializable {
+		private String queries;
+	}
+
+	public void service(Parameters parameters, EntityManager entityManager,
+			JsonResponseWriter response) throws IOException {
+
+		// Obtain the number of queries
+		int queryCount = LogicUtil.getQueryCount(parameters.queries);
+
+		// Create the array of World objects
+		World[] list = new World[queryCount];
+		for (int i = 0; i < list.length; i++) {
+			int identifier = LogicUtil.generateRandomNumber(1, 10000);
+			list[i] = entityManager.find(World.class, identifier);
+		}
+
+		// Send the response
+		response.writeResponse(list);
+	}
+
+}

+ 30 - 0
frameworks/Java/officefloor/src/main/java/net/officefloor/performance/logic/SingleDatabaseQueryLogic.java

@@ -0,0 +1,30 @@
+package net.officefloor.performance.logic;
+
+import java.io.IOException;
+
+import javax.persistence.EntityManager;
+
+import net.officefloor.performance.entities.World;
+import net.officefloor.plugin.json.JsonResponseWriter;
+
+/**
+ * Logic for Single Database Query.
+ * 
+ * @author Daniel Sagenschneider
+ */
+public class SingleDatabaseQueryLogic {
+
+	public void service(EntityManager entityManager, JsonResponseWriter response)
+			throws IOException {
+
+		// Obtain the identifier
+		int identifier = LogicUtil.generateRandomNumber(1, 10000);
+
+		// Retrieve the row
+		World world = entityManager.find(World.class, identifier);
+
+		// Write the response
+		response.writeResponse(world);
+	}
+
+}

+ 18 - 0
frameworks/Java/officefloor/src/main/resources/META-INF/persistence.xml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<persistence xmlns="http://java.sun.com/xml/ns/persistence"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://java.sun.com/xml/ns/persistence 
+        http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
+	version="2.0">
+
+	<persistence-unit name="techempower">
+		<provider>org.datanucleus.api.jpa.PersistenceProviderImpl</provider>
+		<properties>			
+			<!-- Generic properties -->
+			<property name="datanucleus.Multithreaded" value="true" />
+			<property name="datanucleus.cache.level2.type" value="none" />
+			<property name="datanucleus.validation.mode" value="none" />
+		</properties>
+	</persistence-unit>
+
+</persistence>

+ 9 - 0
frameworks/Java/officefloor/src/main/resources/application.objects

@@ -0,0 +1,9 @@
+<objects>
+
+	<managed-object source="net.officefloor.plugin.jpa.JpaEntityManagerManagedObjectSource" type="javax.persistence.EntityManager">
+		<property name="persistence.unit.name" value="techempower" />
+		<property-file path="datasource.properties" />
+		<team name="CLOSE" type="javax.persistence.EntityManager" />
+	</managed-object>
+
+</objects>

+ 5 - 0
frameworks/Java/officefloor/src/main/resources/application.teams

@@ -0,0 +1,5 @@
+<teams>
+
+	<team source="net.officefloor.frame.impl.spi.team.ExecutorCachedTeamSource" type="javax.persistence.EntityManager" />
+
+</teams>

+ 26 - 0
frameworks/Java/officefloor/src/main/resources/application.woof

@@ -0,0 +1,26 @@
+<woof>
+  <templates>
+    <template name="databaseUpdate" uri="databaseUpdate" path="service.woof.html" extends="" class="net.officefloor.performance.logic.DatabaseUpdateLogic" content-type="" secure="false" continue-rendering="false" x="95" y="244">
+    </template>
+    <template name="fortune" uri="fortune" path="fortune.woof.html" extends="" class="net.officefloor.performance.logic.FortunesLogic" content-type="" secure="false" continue-rendering="false" x="100" y="303">
+    </template>
+    <template name="json" uri="json" path="service.woof.html" extends="" class="net.officefloor.performance.logic.JsonSerialisationLogic" content-type="" secure="false" continue-rendering="false" x="84" y="34">
+    </template>
+    <template name="multipleQueries" uri="multipleQueries" path="service.woof.html" extends="" class="net.officefloor.performance.logic.MultipleDatabaseQueriesLogic" content-type="" secure="false" continue-rendering="false" x="91" y="182">
+    </template>
+    <template name="plaintext" uri="plaintext" path="plaintext.woof.html" extends="" class="" content-type="text/plain" secure="false" continue-rendering="false" x="107" y="386">
+    </template>
+    <template name="singleQuery" uri="singleQuery" path="service.woof.html" extends="" class="net.officefloor.performance.logic.SingleDatabaseQueryLogic" content-type="" secure="false" continue-rendering="false" x="87" y="105">
+    </template>
+  </templates>
+  <sections>
+  </sections>
+  <governances>
+  </governances>
+  <resources>
+  </resources>
+  <exceptions>
+  </exceptions>
+  <starting>
+  </starting>
+</woof>

+ 7 - 0
frameworks/Java/officefloor/src/main/resources/c3p0.properties

@@ -0,0 +1,7 @@
+# Pooling of Connections
+datanucleus.connectionPool.maxPoolSize=256
+datanucleus.connectionPool.minPoolSize=32
+datanucleus.connectionPool.initialPoolSize=32
+
+# Pooling of PreparedStatements
+datanucleus.connectionPool.maxStatements=256

+ 5 - 0
frameworks/Java/officefloor/src/main/resources/datasource.properties

@@ -0,0 +1,5 @@
+datanucleus.ConnectionDriverName = org.h2.Driver
+datanucleus.ConnectionURL = jdbc:h2:mem:exampleDb
+datanucleus.ConnectionUserName = sa
+datanucleus.ConnectionPassword = 
+datanucleus.connectionPoolingType = C3P0

+ 11 - 0
frameworks/Java/officefloor/src/main/resources/log4j.properties

@@ -0,0 +1,11 @@
+# Root logger option
+log4j.rootLogger=ERROR, stdout
+ 
+# Direct log messages to stdout
+log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.Target=System.out
+log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
+
+# Stop logging Data Nucleus
+log4j.category.DataNucleus=OFF

+ 7 - 0
frameworks/Java/officefloor/src/main/webapp/WEB-INF/web.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<web-app>
+
+	<display-name>WoOF Application</display-name>
+	<description>Generated from WoOF archetype</description>
+
+</web-app>

+ 11 - 0
frameworks/Java/officefloor/src/main/webapp/fortune.woof.html

@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head><title>Fortunes</title></head>
+<body>
+<table>
+<tr><th>id</th><th>message</th></tr>
+<!-- ${fortunes --><tr><td>${id}</td><td>${message}</td></tr>
+<!-- $} -->
+</table>
+</body>
+</html>

+ 1 - 0
frameworks/Java/officefloor/src/main/webapp/plaintext.woof.html

@@ -0,0 +1 @@
+Hello, World!

+ 5 - 0
frameworks/Java/officefloor/src/main/webapp/service.woof.html

@@ -0,0 +1,5 @@
+<html>
+	<body>
+	<a href="#{service}">Link to invoke service method</a>
+	</bod>
+</html>

+ 256 - 0
frameworks/Java/officefloor/src/test/java/net/officefloor/performance/AbstractDatabaseQueryTestCase.java

@@ -0,0 +1,256 @@
+package net.officefloor.performance;
+
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import lombok.Data;
+import net.officefloor.plugin.woof.WoofOfficeFloorSource;
+
+import org.h2.jdbcx.JdbcConnectionPool;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.type.ArrayType;
+
+/**
+ * Abstract functionality for a database query test.
+ * 
+ * @author Daniel Sagenschneider
+ */
+public abstract class AbstractDatabaseQueryTestCase extends AbstractTestCase {
+
+	/**
+	 * URL for the database.
+	 */
+	private static final String DATABASE_URL = "jdbc:h2:mem:exampleDb";
+
+	/**
+	 * User for the database.
+	 */
+	private static final String DATABASE_USER = "sa";
+
+	/**
+	 * Random numbers for validating results.
+	 */
+	protected final int[] randomNumbers = new int[10000];
+
+	/**
+	 * {@link ObjectMapper}.
+	 */
+	private final ObjectMapper mapper = new ObjectMapper();
+
+	/**
+	 * {@link ArrayType} for parsing the JSON.
+	 */
+	private final ArrayType arrayType = this.mapper.getTypeFactory()
+			.constructArrayType(Result.class);
+
+	/**
+	 * Number of items to request.
+	 */
+	private volatile int requestBatchIndex = 0;
+
+	/**
+	 * Listing of request batch sizes.
+	 */
+	private final String[] queryValues = new String[] { null, "INVALID", "0",
+			"1", "5", "10", "15", "20", "500", "501" };
+
+	/**
+	 * Corresponding list of the resulting request batch size.
+	 */
+	private final int[] requestBatchSizes = new int[] { 1, 1, 1, 1, 5, 10, 15,
+			20, 500, 500 };
+
+	/**
+	 * {@link Connection}.
+	 */
+	protected Connection connection;
+
+	/**
+	 * Result from JSON response.
+	 */
+	@Data
+	public static class Result {
+		private int id;
+		private int randomNumber;
+	}
+
+	@Before
+	public void runApplication() throws Exception {
+		// Start application after database setup
+	}
+
+	@Before
+	public void setupDatabase() throws Exception {
+
+		// Generate the random number values
+		for (int i = 0; i < this.randomNumbers.length; i++) {
+			this.randomNumbers[i] = (int) Math.round(Math.random() * 10000);
+		}
+
+		// Obtain connection via DataSource (sets up in memory database)
+		JdbcConnectionPool dataSource = JdbcConnectionPool.create(DATABASE_URL,
+				DATABASE_USER, "");
+		this.connection = dataSource.getConnection();
+
+		// Create the table
+		Statement statement = this.connection.createStatement();
+		statement
+				.execute("CREATE TABLE World (id int primary key, randomNumber int)");
+
+		// Load the values
+		PreparedStatement insert = this.connection
+				.prepareStatement("INSERT INTO World (id, randomNumber) VALUES (?, ?)");
+		for (int i = 0; i < this.randomNumbers.length; i++) {
+			insert.setInt(1, (i + 1));
+			insert.setInt(2, this.randomNumbers[i]);
+			insert.executeUpdate();
+		}
+
+		// Wait until table available on another connection
+		this.waitForTableToBeAvailable("World", dataSource);
+
+		// Start the application
+		WoofOfficeFloorSource.start();
+	}
+
+	@After
+	public void cleanupDatabase() throws SQLException {
+
+		// Allow some time for clean up of server
+		try {
+			Thread.sleep(1000);
+		} catch (InterruptedException ex) {
+			// Carry on
+		}
+
+		// Stop database for new instance each test
+		this.connection.createStatement().execute("SHUTDOWN IMMEDIATELY");
+
+		// Allow some time for shutdown of database
+		try {
+			Thread.sleep(1000);
+		} catch (InterruptedException ex) {
+			// Carry on
+		}
+	}
+
+	@Override
+	protected int getIterationCount() {
+		return 1000;
+	}
+
+	/**
+	 * Obtains the next index.
+	 * 
+	 * @return Next index.
+	 */
+	protected int nextIndex() {
+		// Obtain the next index
+		int index = this.requestBatchIndex;
+
+		// Provide the index for next request (duplicate batch sizes ok)
+		this.requestBatchIndex = (index + 1) % this.requestBatchSizes.length;
+
+		// Return the index
+		return index;
+	}
+
+	/**
+	 * Obtains the query string.
+	 * 
+	 * @param index
+	 *            Batch index.
+	 * @return Query string.
+	 */
+	protected String getQueryString(int index) {
+		String queryString = this.queryValues[index];
+		queryString = (queryString == null ? "" : "?queries=" + queryString);
+		return queryString;
+	}
+
+	/**
+	 * Obtains the expected batch size.
+	 * 
+	 * @param index
+	 *            Batch index.
+	 * @return Expected batch size.
+	 */
+	protected int getBatchSize(int index) {
+		return this.requestBatchSizes[index];
+	}
+
+	/**
+	 * Reads the {@link Result}.
+	 * 
+	 * @param entity
+	 *            Entity content.
+	 * @return {@link Result}.
+	 * @throws IOException
+	 *             If fails to read {@link Result}.
+	 */
+	protected Result readResult(String entity) throws IOException {
+		return this.mapper.readValue(entity, Result.class);
+	}
+
+	@Test
+	public void ensureReadResult() throws IOException {
+		String entity = "{\"id\":3217,\"randomNumber\":2149}";
+		Result result = this.mapper.readValue(entity, Result.class);
+		assertResult(result, 3217, 2149);
+	}
+
+	/**
+	 * Reads the {@link Result} instances from the entity.
+	 * 
+	 * @param entity
+	 *            Entity content.
+	 * @return {@link Result} instances.
+	 * @throws IOException
+	 *             If fails to read {@link Result} instances.
+	 */
+	protected Result[] readResults(String entity) throws IOException {
+		return (Result[]) this.mapper.readValue(entity, this.arrayType);
+	}
+
+	@Test
+	public void ensureReadResults() throws IOException {
+		String entity = "[{\"id\":4174,\"randomNumber\":331}"
+				+ ",{\"id\":51,\"randomNumber\":6544}"
+				+ ",{\"id\":4462,\"randomNumber\":952}"
+				+ ",{\"id\":2221,\"randomNumber\":532}"
+				+ ",{\"id\":9276,\"randomNumber\":3097}"
+				+ ",{\"id\":3056,\"randomNumber\":7293}"
+				+ ",{\"id\":6964,\"randomNumber\":620}"
+				+ ",{\"id\":675,\"randomNumber\":6601}"
+				+ ",{\"id\":8414,\"randomNumber\":6569}"
+				+ ",{\"id\":2753,\"randomNumber\":4065}]";
+		Result[] results = (Result[]) this.mapper.readValue(entity,
+				this.arrayType);
+		Assert.assertEquals("Incorrect number of elements", 10, results.length);
+		assertResult(results[0], 4174, 331);
+		assertResult(results[1], 51, 6544);
+		assertResult(results[2], 4462, 952);
+		assertResult(results[3], 2221, 532);
+		assertResult(results[4], 9276, 3097);
+		assertResult(results[5], 3056, 7293);
+		assertResult(results[6], 6964, 620);
+		assertResult(results[7], 675, 6601);
+		assertResult(results[8], 8414, 6569);
+		assertResult(results[9], 2753, 4065);
+	}
+
+	private static void assertResult(Result result, int id, int randomNumber) {
+		Assert.assertEquals("Incorrect identifier", id, result.getId());
+		Assert.assertEquals("Incorrect random number", randomNumber,
+				result.getRandomNumber());
+	}
+
+}

+ 372 - 0
frameworks/Java/officefloor/src/test/java/net/officefloor/performance/AbstractTestCase.java

@@ -0,0 +1,372 @@
+package net.officefloor.performance;
+
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.SQLException;
+
+import javax.sql.DataSource;
+
+import net.officefloor.plugin.socket.server.http.HttpHeader;
+import net.officefloor.plugin.woof.WoofOfficeFloorSource;
+
+import org.apache.http.Header;
+import org.apache.http.HttpResponse;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.util.EntityUtils;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Abstract functionality for test.
+ * 
+ * @author Daniel Sagenschneider
+ */
+public abstract class AbstractTestCase {
+
+	/**
+	 * Overridden by child tests for specific test behaviour.
+	 * 
+	 * @param client
+	 *            {@link CloseableHttpClient}.
+	 * @throws Exception
+	 *             If fails.
+	 */
+	protected abstract void doRequestTest(CloseableHttpClient client)
+			throws Exception;
+
+	/*
+	 * ================= Generic test functionality ==========================
+	 */
+
+	@Before
+	public void runApplication() throws Exception {
+		// Start the application
+		WoofOfficeFloorSource.start();
+	}
+
+	@After
+	public void stopApplication() throws Exception {
+		// Stop the application
+		WoofOfficeFloorSource.stop();
+	}
+
+	@Test
+	public void singleRequest() throws Exception {
+		try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
+			this.doRequestTest(client);
+		}
+	}
+
+	@Test
+	public void performance() throws Throwable {
+
+		// Avoid performance test if indicate to skip
+		if (!("yes".equalsIgnoreCase(System.getProperty("doPerformanceTests",
+				"no")))) {
+			System.out.println("Skipping performance test for "
+					+ this.getClass().getSimpleName());
+			return;
+		}
+
+		final int threadCount = this.getParallelClientCount();
+		final int iterationCount = this.getIterationCount();
+
+		// Warm up the server
+		System.out.println("===========================================");
+		System.out.println(this.getClass().getSimpleName()
+				+ "    (running with " + threadCount + " threads and "
+				+ iterationCount + " iterations)");
+		System.out.print("Warming up server ");
+		System.out.flush();
+		try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
+			for (int i = 0; i < (iterationCount * 2); i++) {
+				this.doRequestTest(client);
+				if (i % 1000 == 0) {
+					System.out.print(".");
+					System.out.flush();
+				}
+			}
+		}
+		System.out.println(" warmed up");
+
+		// Create the threads
+		Thread[] threads = new Thread[threadCount];
+		final boolean[] threadComplete = new boolean[threads.length];
+		final Throwable[] failures = new Throwable[threads.length];
+		for (int t = 0; t < threads.length; t++) {
+			final int threadIndex = t;
+			threadComplete[threadIndex] = false; // Not yet complete
+			failures[threadIndex] = null; // no initial failure
+			threads[threadIndex] = new Thread() {
+				@Override
+				public void run() {
+					try {
+						try (CloseableHttpClient client = HttpClientBuilder
+								.create().build()) {
+
+							// Determine progress mark
+							int progressMark = iterationCount / 10;
+							int progressValue = 0;
+
+							// Under the request so many times
+							for (int i = 0; i < iterationCount; i++) {
+
+								// Undertake the request
+								AbstractTestCase.this.doRequestTest(client);
+
+								// Provide progress
+								if ((i % progressMark) == 0) {
+									System.out.print((progressValue++) + ", ");
+
+									// Provide new line to avoid off to right
+									if (threadIndex == 0) {
+										System.out.println();
+									}
+								}
+							}
+						}
+					} catch (Throwable ex) {
+						// Record failure
+						synchronized (failures) {
+							failures[threadIndex] = ex;
+						}
+					} finally {
+						// Notify complete
+						synchronized (threadComplete) {
+							threadComplete[threadIndex] = true;
+							threadComplete.notify();
+						}
+					}
+				}
+			};
+		}
+
+		// Capture start time
+		long startTime = System.currentTimeMillis();
+
+		// Kick off the threads
+		System.out.print("Running requests ...");
+		System.out.flush();
+		for (int t = 0; t < threads.length; t++) {
+			threads[t].start();
+		}
+
+		// Wait for completion
+		synchronized (threadComplete) {
+
+			// Determine if complete
+			boolean isComplete = false;
+			do {
+				// Determine if all threads a complete
+				isComplete = true;
+				for (int t = 0; t < threads.length; t++) {
+					if (!threadComplete[t]) {
+						isComplete = false;
+					}
+				}
+
+				// Wait some time if not complete
+				if (!isComplete) {
+					threadComplete.wait(1000);
+				}
+
+			} while (!isComplete);
+		}
+
+		// Capture the end time
+		long endTime = System.currentTimeMillis();
+		System.out.println(" requests completed");
+
+		// Report any failures
+		Throwable firstFailure = null;
+		synchronized (failures) {
+			for (int t = 0; t < threads.length; t++) {
+				Throwable failure = failures[t];
+				if (failure != null) {
+
+					// Log the failure
+					System.err.println("=========== Failure for client " + t
+							+ " ===========");
+					failure.printStackTrace();
+
+					// Capture the first failure
+					if (firstFailure == null) {
+						firstFailure = failure;
+					}
+				}
+			}
+		}
+		if (firstFailure != null) {
+			// Propagate the failure
+			throw firstFailure;
+		}
+
+		// Determine statistics
+		long duration = endTime - startTime;
+		long totalRequestCount = threadCount * iterationCount;
+		System.out.println();
+		System.out.println(this.getClass().getSimpleName()
+				+ " performance results:");
+		System.out.println(totalRequestCount + " requests (" + threadCount
+				+ " threads by " + iterationCount + " requests) completed in "
+				+ duration + " milliseconds");
+		System.out.println("Effectively 1 request every "
+				+ (duration / (totalRequestCount * 1.0)) + " milliseconds");
+	}
+
+	/**
+	 * Waits for the specified table to be available.
+	 * 
+	 * @param tableName
+	 *            Name of the table.
+	 * @param dataSource
+	 *            {@link DataSource}.
+	 * @throws SQLException
+	 *             If fails to check if table available.
+	 */
+	protected void waitForTableToBeAvailable(String tableName,
+			DataSource dataSource) throws SQLException {
+
+		boolean isTableAvailable = false;
+		long startTime = System.currentTimeMillis();
+		do {
+
+			// Attempt to determine if table available
+			Connection connection = dataSource.getConnection();
+			try {
+				connection.createStatement().executeQuery(
+						"SELECT * FROM " + tableName);
+				isTableAvailable = true;
+			} catch (SQLException ex) {
+				isTableAvailable = false;
+			} finally {
+				connection.close();
+			}
+
+			if (!isTableAvailable) {
+
+				// Determine if time out
+				if ((System.currentTimeMillis() - 10000) > startTime) {
+					Assert.fail("Time out in waiting for World table to be available");
+				}
+
+				// Allow some time for start up of database
+				try {
+					Thread.sleep(1000);
+				} catch (InterruptedException ex) {
+					// Carry on
+				}
+			}
+
+		} while (!isTableAvailable);
+	}
+
+	/**
+	 * Obtains the number of parallel clients.
+	 * 
+	 * @return Number of parallel clients.
+	 */
+	protected int getParallelClientCount() {
+		return 100;
+	}
+
+	/**
+	 * Obtains the iteration count for performance testing.
+	 * 
+	 * @return Iteration count for performance testing.
+	 */
+	protected int getIterationCount() {
+		return 10000;
+	}
+
+	/**
+	 * Asserts the {@link HttpResponse} is correct.
+	 * 
+	 * @param response
+	 *            {@link HttpResponse}.
+	 * @param statusCode
+	 *            Expected status code.
+	 * @param entity
+	 *            Expected entity.
+	 * @param headerNames
+	 *            Expected {@link Header} instances.
+	 * @throws IOException
+	 *             If fails to obtain details from {@link HttpResponse}.
+	 */
+	protected void assertResponse(HttpResponse response, int statusCode,
+			String entity, String... headerNames) throws IOException {
+
+		// Obtain the entity
+		String actualEntity = EntityUtils.toString(response.getEntity());
+
+		// Ensure correct status code
+		Assert.assertEquals("Should be successful: " + actualEntity, 200,
+				response.getStatusLine().getStatusCode());
+
+		// Validate the content
+		Assert.assertEquals("Incorrect entity", entity, actualEntity);
+
+		// Ensure have appropriate headers
+		this.assertHeadersSet(response, headerNames);
+
+	}
+
+	/**
+	 * Ensure all appropriate headers exist.
+	 * 
+	 * @param response
+	 *            {@link HttpResponse}.
+	 * @param headerNames
+	 *            Expected header names.
+	 */
+	protected void assertHeadersSet(HttpResponse response,
+			String... headerNames) {
+
+		// Obtain the headers
+		Header[] headers = response.getAllHeaders();
+
+		// Create the listing of expected headers
+		StringBuilder expectedHeadersText = new StringBuilder();
+		boolean isFirst = true;
+		for (String headerName : headerNames) {
+			if (!isFirst) {
+				expectedHeadersText.append(", ");
+			}
+			expectedHeadersText.append(headerName);
+		}
+
+		// Create the header text
+		StringBuilder actualHeaderText = new StringBuilder();
+		for (Header header : headers) {
+			actualHeaderText.append(header.getName() + ": " + header.getValue()
+					+ "\n");
+		}
+
+		// Validate correct number of headers
+		Assert.assertEquals("Incorrect number of headers ("
+				+ expectedHeadersText.toString() + ")\n\n" + actualHeaderText,
+				headerNames.length, headers.length);
+	}
+
+	/**
+	 * Asserts the {@link HttpHeader} is correct.
+	 * 
+	 * @param response
+	 *            {@link HttpResponse}.
+	 * @param headerName
+	 *            {@link HttpHeader} name.
+	 * @param headerValue
+	 *            Expected {@link HttpHeader} value.
+	 */
+	protected void assertHeader(HttpResponse response, String headerName,
+			String headerValue) {
+		Header header = response.getFirstHeader(headerName);
+		Assert.assertNotNull("Should have header " + headerName, header);
+		Assert.assertEquals("Incorrect value for header " + headerName,
+				headerValue, header.getValue());
+	}
+
+}

+ 113 - 0
frameworks/Java/officefloor/src/test/java/net/officefloor/performance/DatabaseUpdateTest.java

@@ -0,0 +1,113 @@
+package net.officefloor.performance;
+
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.util.EntityUtils;
+import org.junit.Assert;
+
+/**
+ * Ensures meets criteria for Test type 5: Database updates.
+ * 
+ * @author Daniel Sagenschneider
+ */
+public class DatabaseUpdateTest extends AbstractDatabaseQueryTestCase {
+
+	@Override
+	protected int getParallelClientCount() {
+		return 10;
+	}
+
+	@Override
+	protected int getIterationCount() {
+		return 100;
+	}
+
+	@Override
+	protected void doRequestTest(CloseableHttpClient client) throws Exception {
+
+		// Obtain the next index
+		int index = this.nextIndex();
+
+		// Obtain the query string
+		String queryString = this.getQueryString(index);
+
+		// Obtain the expected batch size
+		int batchSize = this.getBatchSize(index);
+
+		// Request the results
+		HttpResponse response = client.execute(new HttpGet(
+				"http://localhost:7878/databaseUpdate-service.woof"
+						+ queryString));
+
+		// Obtain the entity
+		String entity = EntityUtils.toString(response.getEntity());
+
+		// Validate the response
+		Assert.assertEquals("Should be successful: " + entity, 200, response
+				.getStatusLine().getStatusCode());
+		this.assertHeadersSet(response, "Content-Length", "Content-Type",
+				"Server", "Date", "set-cookie");
+		this.assertHeader(response, "Content-Type",
+				"application/json; charset=UTF-8");
+
+		// Validate the correct random result
+		Result[] results = (Result[]) this.readResults(entity);
+		Assert.assertEquals("Incorrect number of results", batchSize,
+				results.length);
+
+		// Ensure response value different to original value
+		boolean isDifferent = false;
+		for (Result result : results) {
+			int responseValue = result.getRandomNumber();
+			int originalValue = this.randomNumbers[result.getId() - 1];
+			if (responseValue != originalValue) {
+				isDifferent = true;
+			}
+		}
+		Assert.assertTrue(
+				"Should have at least one value different (if batch size big enough)",
+				isDifferent || (batchSize <= 2));
+
+		// Ensure values in database are different (ie have been updated)
+		isDifferent = false;
+		for (Result result : results) {
+			int databaseValue = this.getDatabaseRandomNumber(result.getId());
+			int originalValue = this.randomNumbers[result.getId() - 1];
+			if (databaseValue != originalValue) {
+				isDifferent = true;
+			}
+		}
+		Assert.assertTrue(
+				"Database should have changed (except unlikely case of same random number)",
+				isDifferent || (batchSize <= 2));
+	}
+
+	/**
+	 * Obtains the random number in the database for the identifier.
+	 * 
+	 * @param identifier
+	 *            Identifier.
+	 * @return Random number.
+	 * @throws SQLException
+	 *             If fails to obtain the random number.
+	 */
+	private int getDatabaseRandomNumber(int identifier) throws SQLException {
+		PreparedStatement selectStatement = this.connection
+				.prepareStatement("SELECT id, randomNumber FROM World WHERE id = ?");
+		selectStatement.setInt(1, identifier);
+		ResultSet resultSet = selectStatement.executeQuery();
+		try {
+			Assert.assertTrue("No result for identifier " + identifier,
+					resultSet.next());
+			return resultSet.getInt("randomNumber");
+		} finally {
+			resultSet.close();
+		}
+	}
+
+}

+ 155 - 0
frameworks/Java/officefloor/src/test/java/net/officefloor/performance/FortunesTest.java

@@ -0,0 +1,155 @@
+package net.officefloor.performance;
+
+import java.nio.charset.Charset;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import net.officefloor.plugin.woof.WoofOfficeFloorSource;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.h2.jdbcx.JdbcConnectionPool;
+import org.junit.After;
+import org.junit.Before;
+
+/**
+ * Ensures meets criteria for Test type 4: Fortunes.
+ * 
+ * @author Daniel Sagenschneider.
+ */
+public class FortunesTest extends AbstractTestCase {
+
+	/**
+	 * Expected response.
+	 */
+	private static final String EXPECTED_RESPONSE = "<!DOCTYPE html>\n<html>\n"
+			+ "<head><title>Fortunes</title></head>\n"
+			+ "<body>\n<table>\n<tr><th>id</th><th>message</th></tr>\n"
+			+ "<tr><td>11</td><td>&lt;script&gt;alert(&quot;This should not be displayed in a browser alert box.&quot;);&lt;/script&gt;</td></tr>\n"
+			+ "<tr><td>4</td><td>A bad random number generator: 1, 1, 1, 1, 1, 4.33e+67, 1, 1, 1</td></tr>\n"
+			+ "<tr><td>5</td><td>A computer program does what you tell it to do, not what you want it to do.</td></tr>\n"
+			+ "<tr><td>2</td><td>A computer scientist is someone who fixes things that aren't broken.</td></tr>\n"
+			+ "<tr><td>8</td><td>A list is only as strong as its weakest link. &mdash; Donald Knuth</td></tr>\n"
+			+ "<tr><td>0</td><td>Additional fortune added at request time.</td></tr>\n"
+			+ "<tr><td>3</td><td>After enough decimal places, nobody gives a damn.</td></tr>\n"
+			+ "<tr><td>7</td><td>Any program that runs right is obsolete.</td></tr>\n"
+			+ "<tr><td>10</td><td>Computers make very fast, very accurate mistakes.</td></tr>\n"
+			+ "<tr><td>6</td><td>Emacs is a nice operating system, but I prefer UNIX. &mdash; Tom Christaensen</td></tr>\n"
+			+ "<tr><td>9</td><td>Feature: A bug with seniority.</td></tr>\n"
+			+ "<tr><td>1</td><td>fortune: No such file or directory</td></tr>\n"
+			+ "<tr><td>12</td><td>フレームワークのベンチマーク</td></tr>\n"
+			+ "\n</table>\n</body>\n</html>";
+
+	/**
+	 * URL for the database.
+	 */
+	private static final String DATABASE_URL = "jdbc:h2:mem:exampleDb";
+
+	/**
+	 * User for the database.
+	 */
+	private static final String DATABASE_USER = "sa";
+
+	@Before
+	public void runApplication() throws Exception {
+		// Start application after database setup
+	}
+
+	@Before
+	public void setupDatabase() throws Exception {
+
+		// Obtain connection via DataSource (sets up in memory database)
+		JdbcConnectionPool dataSource = JdbcConnectionPool.create(DATABASE_URL,
+				DATABASE_USER, "");
+		Connection connection = dataSource.getConnection();
+
+		// Create the table
+		Statement statement = connection.createStatement();
+		statement
+				.execute("CREATE TABLE Fortune (id int primary key, message varchar(200))");
+
+		// Load the values
+		PreparedStatement insert = connection
+				.prepareStatement("INSERT INTO Fortune (id, message) VALUES (?, ?)");
+		this.loadEntry(insert, 1, "fortune: No such file or directory");
+		this.loadEntry(insert, 2,
+				"A computer scientist is someone who fixes things that aren't broken.");
+		this.loadEntry(insert, 3,
+				"After enough decimal places, nobody gives a damn.");
+		this.loadEntry(insert, 4,
+				"A bad random number generator: 1, 1, 1, 1, 1, 4.33e+67, 1, 1, 1");
+		this.loadEntry(insert, 5,
+				"A computer program does what you tell it to do, not what you want it to do.");
+		this.loadEntry(insert, 6,
+				"Emacs is a nice operating system, but I prefer UNIX. — Tom Christaensen");
+		this.loadEntry(insert, 7, "Any program that runs right is obsolete.");
+		this.loadEntry(insert, 8,
+				"A list is only as strong as its weakest link. — Donald Knuth");
+		this.loadEntry(insert, 9, "Feature: A bug with seniority.");
+		this.loadEntry(insert, 10,
+				"Computers make very fast, very accurate mistakes.");
+		this.loadEntry(
+				insert,
+				11,
+				"<script>alert(\"This should not be displayed in a browser alert box.\");</script>");
+		this.loadEntry(insert, 12, "フレームワークのベンチマーク");
+
+		// Ensure table is available
+		this.waitForTableToBeAvailable("Fortune", dataSource);
+
+		// Start the application
+		WoofOfficeFloorSource.start();
+	}
+
+	@Override
+	protected int getIterationCount() {
+		return 1000;
+	}
+
+	/**
+	 * Loads an row into the table.
+	 * 
+	 * @param insertStatement
+	 *            {@link PreparedStatement}.
+	 * @param identifier
+	 *            Identifier.
+	 * @param message
+	 *            Message.
+	 * @throws SQLException
+	 *             If fails to insert the row.
+	 */
+	private void loadEntry(PreparedStatement insertStatement, int identifier,
+			String message) throws SQLException {
+		insertStatement.setInt(1, identifier);
+		insertStatement.setString(2, message);
+		insertStatement.executeUpdate();
+	}
+
+	@After
+	public void cleanupDatabase() throws SQLException {
+		// Stop database for new instance each test
+		DriverManager.getConnection(DATABASE_URL, DATABASE_USER, "")
+				.createStatement().execute("SHUTDOWN IMMEDIATELY");
+	}
+
+	@Override
+	protected void doRequestTest(CloseableHttpClient client) throws Exception {
+
+		// Request the fortunes
+		HttpResponse response = client.execute(new HttpGet(
+				"http://localhost:7878/fortune.woof"));
+
+		// Validate the response
+		this.assertResponse(response, 200, EXPECTED_RESPONSE, "Content-Length",
+				"Content-Type", "Server", "Date", "set-cookie");
+		this.assertHeader(response, "Content-Type", "text/html; charset=UTF-8");
+		this.assertHeader(response, "Content-Length",
+				String.valueOf(EXPECTED_RESPONSE.getBytes(Charset
+						.forName("UTF-8")).length));
+	}
+
+}

+ 30 - 0
frameworks/Java/officefloor/src/test/java/net/officefloor/performance/JsonSerialisationTest.java

@@ -0,0 +1,30 @@
+package net.officefloor.performance;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+
+/**
+ * Ensure meet criteria for Test type 1: JSON serialization.
+ * 
+ * @author Daniel Sagenschneider
+ */
+public class JsonSerialisationTest extends AbstractTestCase {
+
+	@Override
+	protected void doRequestTest(CloseableHttpClient client) throws Exception {
+
+		// Request the JSON serialisation
+		HttpResponse response = client.execute(new HttpGet(
+				"http://localhost:7878/json-service.woof"));
+
+		// Validate the response
+		this.assertResponse(response, 200, "{\"message\":\"Hello, World!\"}",
+				"Content-Length", "Content-Type", "Server", "Date",
+				"set-cookie");
+		this.assertHeader(response, "Content-Type",
+				"application/json; charset=UTF-8");
+		this.assertHeader(response, "Content-Length", "27");
+	}
+
+}

+ 61 - 0
frameworks/Java/officefloor/src/test/java/net/officefloor/performance/MultipleDatabaseQueriesTest.java

@@ -0,0 +1,61 @@
+package net.officefloor.performance;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.util.EntityUtils;
+import org.junit.Assert;
+
+/**
+ * Ensures meets criteria for Test type 3: Multiple database queries.
+ * 
+ * @author Daniel Sagenschneider.
+ */
+public class MultipleDatabaseQueriesTest extends AbstractDatabaseQueryTestCase {
+
+	@Override
+	protected int getIterationCount() {
+		return 100;
+	}
+
+	@Override
+	protected void doRequestTest(CloseableHttpClient client) throws Exception {
+
+		// Obtain the next index
+		int index = this.nextIndex();
+
+		// Obtain the query string
+		String queryString = this.getQueryString(index);
+
+		// Obtain the expected batch size
+		int batchSize = this.getBatchSize(index);
+
+		// Request the results
+		HttpResponse response = client.execute(new HttpGet(
+				"http://localhost:7878/multipleQueries-service.woof"
+						+ queryString));
+
+		// Obtain the entity
+		String entity = EntityUtils.toString(response.getEntity());
+
+		// Validate the response
+		Assert.assertEquals("Should be successful: " + entity, 200, response
+				.getStatusLine().getStatusCode());
+		this.assertHeadersSet(response, "Content-Length", "Content-Type",
+				"Server", "Date", "set-cookie");
+		this.assertHeader(response, "Content-Type",
+				"application/json; charset=UTF-8");
+
+		// Validate the correct random result
+		Result[] results = (Result[]) this.readResults(entity);
+		Assert.assertEquals("Incorrect number of results", batchSize,
+				results.length);
+		for (int i = 0; i < batchSize; i++) {
+			Result result = results[i];
+			Assert.assertEquals("Incorrect random number for item " + i,
+					this.randomNumbers[result.getId() - 1],
+					result.getRandomNumber());
+		}
+	}
+
+}

+ 28 - 0
frameworks/Java/officefloor/src/test/java/net/officefloor/performance/PlainTextTest.java

@@ -0,0 +1,28 @@
+package net.officefloor.performance;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+
+/**
+ * Ensures meets criteria for Test type 6: Plaintext.
+ * 
+ * @author Daniel Sagenschneider.
+ */
+public class PlainTextTest extends AbstractTestCase {
+
+	@Override
+	protected void doRequestTest(CloseableHttpClient client) throws Exception {
+
+		// Request the plain text
+		HttpResponse response = client.execute(new HttpGet(
+				"http://localhost:7878/plaintext.woof"));
+
+		// Validate the response
+		this.assertResponse(response, 200, "Hello, World!", "Content-Length",
+				"Content-Type", "Server", "Date", "set-cookie");
+		this.assertHeader(response, "Content-Type", "text/plain; charset=UTF-8");
+		this.assertHeader(response, "Content-Length", "13");
+	}
+
+}

+ 41 - 0
frameworks/Java/officefloor/src/test/java/net/officefloor/performance/SingleDatabaseQueryTest.java

@@ -0,0 +1,41 @@
+package net.officefloor.performance;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.util.EntityUtils;
+import org.junit.Assert;
+
+/**
+ * Ensures meets criteria for Test type 2: Single database query.
+ * 
+ * @author Daniel Sagenschneider.
+ */
+public class SingleDatabaseQueryTest extends AbstractDatabaseQueryTestCase {
+
+	@Override
+	protected void doRequestTest(CloseableHttpClient client) throws Exception {
+
+		// Request the random database value
+		HttpResponse response = client.execute(new HttpGet(
+				"http://localhost:7878/singleQuery-service.woof"));
+
+		// Obtain the entity
+		String entity = EntityUtils.toString(response.getEntity());
+
+		// Validate the response
+		Assert.assertEquals("Should be successful: " + entity, 200, response
+				.getStatusLine().getStatusCode());
+		this.assertHeadersSet(response, "Content-Length", "Content-Type",
+				"Server", "Date", "set-cookie");
+		this.assertHeader(response, "Content-Type",
+				"application/json; charset=UTF-8");
+
+		// Validate the correct random result
+		Result result = this.readResult(entity);
+		Assert.assertEquals("Incorrect random number",
+				this.randomNumbers[result.getId() - 1],
+				result.getRandomNumber());
+	}
+
+}