diff --git a/notes.txt b/notes.txt
new file mode 100644
index 00000000..96c29f57
--- /dev/null
+++ b/notes.txt
@@ -0,0 +1,66 @@
+Changes:
+
+CakeEntity (amended)
+ - rename as simply Cake
+ - remove references to Employee
+ - add JsonProperty annotation for description field deserialization
+
+CakeFactory (added)
+ - class added for use when creating cakes
+
+CakeServlet (amended)
+ - fix servlet mappings so servlet runs on "/" and "/cakes" and set so it is initialised on startup
+ - replace System out with Logger (using Logback)
+ - add package private constructor which accepts class dependencies, and no-arg constructor creating class with default version of dependencies
+ - refactor init method so initial cakes are deserialized using jackson, and stored in database using CakeStore class (new)
+ - amend doGet method to check accept header and return JSON / HTML depending on presence of "application/json" value
+ - add doPost method, which uses the CakeFactory to make a cake and the CakeStore to persist it, then simply runs the doGet method to render suitable output to the user
+ - add destroy method to close the Hibernate session on closure
+
+CakeStore (added)
+ - added method for retrieving all cakes from the database
+ - added method for storing individual cakes to the database
+ - rather than living with lots of exceptions when duplicate titles are inserted, check for these in advance and if they are found then update the cake
+
+HibernateUtil
+ - make class final
+ - add private constructor
+
+src/main/resources/initial-cakes.json (added)
+ - copy of the json file previously pulled from github. Held locally to speed up testing, and allow webapp to function without web connection
+
+src/main/resources/logback.xml (added)
+ - added to provide some basic configuration of the log output
+
+src/main/webapp/css/narrow-jumbortron.css (added)
+ - copied from bootstrap.com, used for presentation
+
+src/main/webapp/WEB-INF/web.xml (amended)
+ - fixed web-app tag
+
+src/main/webapp/index.jsp (amended)
+ - changed to present the output in a user friendly manner. Not entirely happy with the look/responsiveness of this but couldnt afford to spend more time on it
+
+CakeFactoryTest (added)
+ - junit tests for the CakeFactory
+
+CakeServletTest (added)
+ - junit tests for the CakeServlet
+
+CakeStoreTest (new)
+ - junit tests for the CakeStore
+
+pom.xml (amended)
+ - changed jackson-core to jackson-databind, and updated version
+ - add slf4j-api and logback-classic
+ - change junit version to 4.12
+ - add mockito and hamcrest
+
+Areas where further work might be done if time allowed
+ - add cucumber acceptance test / some kind of UI test
+ - use some means of object creation / dependency injection such as spring
+ - use a NoSQL DB such as MongoDB rather than HSQL
+ - personal dislike for the HibernateUtil singleton as it makes junit testing difficult
+ - allow json to be written / retrieved direct from database
+ - more work on presentation
+ - potentially rewrite using Node / Mongoose / React / Mongodb
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index c8cbf9d5..fa631d33 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,8 +19,8 @@
com.fasterxml.jackson.core
- jackson-core
- 2.8.0
+ jackson-databind
+ 2.9.2
@@ -36,12 +36,34 @@
hsqldb
2.3.4
+
+ org.slf4j
+ slf4j-api
+ 1.7.25
+
+
+ ch.qos.logback
+ logback-classic
+ 1.2.3
+
junit
junit
- 4.1
+ 4.12
+ test
+
+
+ org.mockito
+ mockito-all
+ 1.10.19
+ test
+
+
+ org.hamcrest
+ hamcrest-all
+ 1.3
test
@@ -62,8 +84,9 @@
org.eclipse.jetty
jetty-maven-plugin
+ 9.4.7.v20170914
- 10
+ 5
STOP
8005
diff --git a/src/main/java/com.waracle.cakemgr/CakeServlet.java b/src/main/java/com.waracle.cakemgr/CakeServlet.java
deleted file mode 100644
index 9bd32f76..00000000
--- a/src/main/java/com.waracle.cakemgr/CakeServlet.java
+++ /dev/null
@@ -1,106 +0,0 @@
-package com.waracle.cakemgr;
-
-import com.fasterxml.jackson.core.JsonFactory;
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.core.JsonToken;
-import org.hibernate.Session;
-import org.hibernate.exception.ConstraintViolationException;
-
-import javax.servlet.ServletException;
-import javax.servlet.annotation.WebServlet;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import java.io.*;
-import java.net.URL;
-import java.util.List;
-
-@WebServlet("/cakes")
-public class CakeServlet extends HttpServlet {
-
- @Override
- public void init() throws ServletException {
- super.init();
-
- System.out.println("init started");
-
-
- System.out.println("downloading cake json");
- try (InputStream inputStream = new URL("https://gist.githubusercontent.com/hart88/198f29ec5114a3ec3460/raw/8dd19a88f9b8d24c23d9960f3300d0c917a4f07c/cake.json").openStream()) {
- BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
-
- StringBuffer buffer = new StringBuffer();
- String line = reader.readLine();
- while (line != null) {
- buffer.append(line);
- line = reader.readLine();
- }
-
- System.out.println("parsing cake json");
- JsonParser parser = new JsonFactory().createParser(buffer.toString());
- if (JsonToken.START_ARRAY != parser.nextToken()) {
- throw new Exception("bad token");
- }
-
- JsonToken nextToken = parser.nextToken();
- while(nextToken == JsonToken.START_OBJECT) {
- System.out.println("creating cake entity");
-
- CakeEntity cakeEntity = new CakeEntity();
- System.out.println(parser.nextFieldName());
- cakeEntity.setTitle(parser.nextTextValue());
-
- System.out.println(parser.nextFieldName());
- cakeEntity.setDescription(parser.nextTextValue());
-
- System.out.println(parser.nextFieldName());
- cakeEntity.setImage(parser.nextTextValue());
-
- Session session = HibernateUtil.getSessionFactory().openSession();
- try {
- session.beginTransaction();
- session.persist(cakeEntity);
- System.out.println("adding cake entity");
- session.getTransaction().commit();
- } catch (ConstraintViolationException ex) {
-
- }
- session.close();
-
- nextToken = parser.nextToken();
- System.out.println(nextToken);
-
- nextToken = parser.nextToken();
- System.out.println(nextToken);
- }
-
- } catch (Exception ex) {
- throw new ServletException(ex);
- }
-
- System.out.println("init finished");
- }
-
- @Override
- protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
-
- Session session = HibernateUtil.getSessionFactory().openSession();
- List list = session.createCriteria(CakeEntity.class).list();
-
- resp.getWriter().println("[");
-
- for (CakeEntity entity : list) {
- resp.getWriter().println("\t{");
-
- resp.getWriter().println("\t\t\"title\" : " + entity.getTitle() + ", ");
- resp.getWriter().println("\t\t\"desc\" : " + entity.getDescription() + ",");
- resp.getWriter().println("\t\t\"image\" : " + entity.getImage());
-
- resp.getWriter().println("\t}");
- }
-
- resp.getWriter().println("]");
-
- }
-
-}
diff --git a/src/main/java/com.waracle.cakemgr/CakeEntity.java b/src/main/java/com/waracle/cakemgr/Cake.java
similarity index 52%
rename from src/main/java/com.waracle.cakemgr/CakeEntity.java
rename to src/main/java/com/waracle/cakemgr/Cake.java
index 7927bd5d..d4c1a624 100644
--- a/src/main/java/com.waracle.cakemgr/CakeEntity.java
+++ b/src/main/java/com/waracle/cakemgr/Cake.java
@@ -1,30 +1,34 @@
package com.waracle.cakemgr;
-import java.io.Serializable;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.hibernate.annotations.DynamicUpdate;
import javax.persistence.*;
+import java.io.Serializable;
@Entity
-@org.hibernate.annotations.Entity(dynamicUpdate = true)
-@Table(name = "Employee", uniqueConstraints = {@UniqueConstraint(columnNames = "ID"), @UniqueConstraint(columnNames = "EMAIL")})
-public class CakeEntity implements Serializable {
-
- private static final long serialVersionUID = -1798070786993154676L;
+@DynamicUpdate
+@Table(name = "Cake", uniqueConstraints = {@UniqueConstraint(columnNames = "ID"), @UniqueConstraint(columnNames = "TITLE")})
+public class Cake implements Serializable {
@Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "ID", unique = true, nullable = false)
- private Integer employeeId;
+ private Integer cakeId;
- @Column(name = "EMAIL", unique = true, nullable = false, length = 100)
+ @Column(name = "TITLE", unique = true, nullable = false, length = 100)
private String title;
- @Column(name = "FIRST_NAME", unique = false, nullable = false, length = 100)
+ @JsonProperty("desc")
+ @Column(name = "DESCRIPTION", nullable = false, length = 100)
private String description;
- @Column(name = "LAST_NAME", unique = false, nullable = false, length = 300)
+ @Column(name = "IMAGE", nullable = false, length = 300)
private String image;
+ public Integer getCakeId() {
+ return cakeId;
+ }
public String getTitle() {
return title;
}
diff --git a/src/main/java/com/waracle/cakemgr/CakeFactory.java b/src/main/java/com/waracle/cakemgr/CakeFactory.java
new file mode 100644
index 00000000..ff57fc4c
--- /dev/null
+++ b/src/main/java/com/waracle/cakemgr/CakeFactory.java
@@ -0,0 +1,29 @@
+package com.waracle.cakemgr;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+
+class CakeFactory {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(CakeFactory.class);
+ private static final String TITLE = "title";
+ private static final String DESCRIPTION = "description";
+ private static final String IMAGE = "image";
+
+ Cake makeCake(final HttpServletRequest req) throws IOException {
+ Cake cake = new Cake();
+ String title = req.getParameter(TITLE);
+ LOGGER.debug("title: {}", title);
+ cake.setTitle(title);
+ String description = req.getParameter(DESCRIPTION);
+ LOGGER.debug("description: {}", description);
+ cake.setDescription(description);
+ String image = req.getParameter(IMAGE);
+ LOGGER.debug("image: {}", image);
+ cake.setImage(image);
+ return cake;
+ }
+}
diff --git a/src/main/java/com/waracle/cakemgr/CakeServlet.java b/src/main/java/com/waracle/cakemgr/CakeServlet.java
new file mode 100644
index 00000000..917e403d
--- /dev/null
+++ b/src/main/java/com/waracle/cakemgr/CakeServlet.java
@@ -0,0 +1,97 @@
+package com.waracle.cakemgr;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+@WebServlet(urlPatterns = {"/", "/cakes"},
+ loadOnStartup = 1)
+public class CakeServlet extends HttpServlet {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(CakeServlet.class);
+ private static final String APPLICATION_JSON = "application/json";
+ private static final String ACCEPT = "accept";
+ private static final String INPUT_JSON = "src/main/resources/initial-cakes.json";
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private final CakeFactory cakeFactory;
+ private final CakeStore cakeStore;
+
+ public CakeServlet() {
+ this(new CakeFactory(), new CakeStore());
+ }
+
+ CakeServlet(CakeFactory cakeFactory, CakeStore cakeStore) {
+ super();
+ this.cakeFactory = cakeFactory;
+ this.cakeStore = cakeStore;
+ }
+
+ @Override
+ public void init() throws ServletException {
+ super.init();
+
+ LOGGER.debug("init started");
+
+ try {
+ fillStore();
+ } catch (IOException ex) {
+ LOGGER.error("error loading initial cake collection", ex);
+ throw new ServletException(ex);
+ }
+
+ LOGGER.info("init finished");
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ LOGGER.debug("GET /cakes");
+ if (req.getHeader(ACCEPT).contains(APPLICATION_JSON)) {
+ resp.getWriter().print(cakesAsJson());
+ } else {
+ req.setAttribute("cakes", cakeStore.allCakes());
+ getServletContext().getRequestDispatcher("/index.jsp").forward(req, resp);
+ }
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ LOGGER.debug("POST /cakes");
+ cakeStore.store(cakeFactory.makeCake(req));
+ doGet(req, resp);
+ }
+
+ @Override
+ public void destroy() {
+ super.destroy();
+ HibernateUtil.shutdown();
+ }
+
+ private String cakesAsJson() throws JsonProcessingException {
+ return MAPPER.writeValueAsString(cakeStore.allCakes());
+ }
+
+
+ private void fillStore() throws IOException {
+ initialCakes().stream().forEach(cake -> cakeStore.store(cake));
+ }
+
+ private List initialCakes() throws IOException {
+ try (InputStream inputStream = new FileInputStream(INPUT_JSON)) {
+ return MAPPER.readValue(inputStream, new TypeReference>(){});
+ }
+ }
+
+}
diff --git a/src/main/java/com/waracle/cakemgr/CakeStore.java b/src/main/java/com/waracle/cakemgr/CakeStore.java
new file mode 100644
index 00000000..1dad0861
--- /dev/null
+++ b/src/main/java/com/waracle/cakemgr/CakeStore.java
@@ -0,0 +1,50 @@
+package com.waracle.cakemgr;
+
+import org.hibernate.Session;
+import org.hibernate.criterion.Restrictions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+public class CakeStore {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(CakeStore.class);
+
+ public List allCakes() {
+ Session session = HibernateUtil.getSessionFactory().openSession();
+ try {
+ return session.createCriteria(Cake.class).list();
+ } finally {
+ session.close();
+ }
+ }
+
+ public void store(final Cake cake) {
+ Session session = HibernateUtil.getSessionFactory().openSession();
+ store(cake, session);
+ }
+
+ Cake store(final Cake cake, final Session session) {
+ try {
+ String title = cake.getTitle();
+ List existingCakes = session.createCriteria(Cake.class).add(Restrictions.eq("title", title)).list();
+ Cake cakeToStore = cake;
+ if (existingCakes.size() > 0) {
+ Cake existingCake = (Cake)existingCakes.get(0);
+ LOGGER.info("found existing cake with matching title: {}. Updating with new details", title);
+ cakeToStore = existingCake;
+ cakeToStore.setTitle(cake.getTitle());
+ cakeToStore.setDescription(cake.getDescription());
+ cakeToStore.setImage(cake.getImage());
+ }
+ session.beginTransaction();
+ session.persist(cakeToStore);
+ session.getTransaction().commit();
+ LOGGER.info("cake stored. id: {}, title", cakeToStore.getCakeId(), title);
+ } finally {
+ session.close();
+ }
+ return cake;
+ }
+}
diff --git a/src/main/java/com.waracle.cakemgr/HibernateUtil.java b/src/main/java/com/waracle/cakemgr/HibernateUtil.java
similarity index 94%
rename from src/main/java/com.waracle.cakemgr/HibernateUtil.java
rename to src/main/java/com/waracle/cakemgr/HibernateUtil.java
index 41ef137b..873fbc8b 100644
--- a/src/main/java/com.waracle.cakemgr/HibernateUtil.java
+++ b/src/main/java/com/waracle/cakemgr/HibernateUtil.java
@@ -5,7 +5,10 @@
import org.hibernate.cfg.Configuration;
import org.hibernate.service.ServiceRegistry;
-public class HibernateUtil {
+public final class HibernateUtil {
+
+ private HibernateUtil(){
+ }
private static SessionFactory sessionFactory = buildSessionFactory();
diff --git a/src/main/resources/hibernate.cfg.xml b/src/main/resources/hibernate.cfg.xml
index 0ae06d63..922b6c65 100644
--- a/src/main/resources/hibernate.cfg.xml
+++ b/src/main/resources/hibernate.cfg.xml
@@ -12,6 +12,6 @@
jdbc:hsqldb:mem:db
create
-
+
\ No newline at end of file
diff --git a/src/main/resources/initial-cakes.json b/src/main/resources/initial-cakes.json
new file mode 100644
index 00000000..6e40dc24
--- /dev/null
+++ b/src/main/resources/initial-cakes.json
@@ -0,0 +1,102 @@
+[
+ {
+ "title": "Lemon cheesecake",
+ "desc": "A cheesecake made of lemon",
+ "image": "https://s3-eu-west-1.amazonaws.com/s3.mediafileserver.co.uk/carnation/WebFiles/RecipeImages/lemoncheesecake_lg.jpg"
+ },
+ {
+ "title": "victoria sponge",
+ "desc": "sponge with jam",
+ "image": "http://www.bbcgoodfood.com/sites/bbcgoodfood.com/files/recipe_images/recipe-image-legacy-id--1001468_10.jpg"
+ },
+ {
+ "title": "Carrot cake",
+ "desc": "Bugs bunnys favourite",
+ "image": "http://www.villageinn.com/i/pies/profile/carrotcake_main1.jpg"
+ },
+ {
+ "title": "Banana cake",
+ "desc": "Donkey kongs favourite",
+ "image": "http://ukcdn.ar-cdn.com/recipes/xlarge/ff22df7f-dbcd-4a09-81f7-9c1d8395d936.jpg"
+ },
+ {
+ "title": "Birthday cake",
+ "desc": "a yearly treat",
+ "image": "http://cornandco.com/wp-content/uploads/2014/05/birthday-cake-popcorn.jpg"
+ },
+ {
+ "title": "Lemon cheesecake",
+ "desc": "A cheesecake made of lemon",
+ "image": "https://s3-eu-west-1.amazonaws.com/s3.mediafileserver.co.uk/carnation/WebFiles/RecipeImages/lemoncheesecake_lg.jpg"
+ },
+ {
+ "title": "victoria sponge",
+ "desc": "sponge with jam",
+ "image": "http://www.bbcgoodfood.com/sites/bbcgoodfood.com/files/recipe_images/recipe-image-legacy-id--1001468_10.jpg"
+ },
+ {
+ "title": "Carrot cake",
+ "desc": "Bugs bunnys favourite",
+ "image": "http://www.villageinn.com/i/pies/profile/carrotcake_main1.jpg"
+ },
+ {
+ "title": "Banana cake",
+ "desc": "Donkey kongs favourite",
+ "image": "http://ukcdn.ar-cdn.com/recipes/xlarge/ff22df7f-dbcd-4a09-81f7-9c1d8395d936.jpg"
+ },
+ {
+ "title": "Birthday cake",
+ "desc": "a yearly treat",
+ "image": "http://cornandco.com/wp-content/uploads/2014/05/birthday-cake-popcorn.jpg"
+ },
+ {
+ "title": "Lemon cheesecake",
+ "desc": "A cheesecake made of lemon",
+ "image": "https://s3-eu-west-1.amazonaws.com/s3.mediafileserver.co.uk/carnation/WebFiles/RecipeImages/lemoncheesecake_lg.jpg"
+ },
+ {
+ "title": "victoria sponge",
+ "desc": "sponge with jam",
+ "image": "http://www.bbcgoodfood.com/sites/bbcgoodfood.com/files/recipe_images/recipe-image-legacy-id--1001468_10.jpg"
+ },
+ {
+ "title": "Carrot cake",
+ "desc": "Bugs bunnys favourite",
+ "image": "http://www.villageinn.com/i/pies/profile/carrotcake_main1.jpg"
+ },
+ {
+ "title": "Banana cake",
+ "desc": "Donkey kongs favourite",
+ "image": "http://ukcdn.ar-cdn.com/recipes/xlarge/ff22df7f-dbcd-4a09-81f7-9c1d8395d936.jpg"
+ },
+ {
+ "title": "Birthday cake",
+ "desc": "a yearly treat",
+ "image": "http://cornandco.com/wp-content/uploads/2014/05/birthday-cake-popcorn.jpg"
+ },
+ {
+ "title": "Lemon cheesecake",
+ "desc": "A cheesecake made of lemon",
+ "image": "https://s3-eu-west-1.amazonaws.com/s3.mediafileserver.co.uk/carnation/WebFiles/RecipeImages/lemoncheesecake_lg.jpg"
+ },
+ {
+ "title": "victoria sponge",
+ "desc": "sponge with jam",
+ "image": "http://www.bbcgoodfood.com/sites/bbcgoodfood.com/files/recipe_images/recipe-image-legacy-id--1001468_10.jpg"
+ },
+ {
+ "title": "Carrot cake",
+ "desc": "Bugs bunnys favourite",
+ "image": "http://www.villageinn.com/i/pies/profile/carrotcake_main1.jpg"
+ },
+ {
+ "title": "Banana cake",
+ "desc": "Donkey kongs favourite",
+ "image": "http://ukcdn.ar-cdn.com/recipes/xlarge/ff22df7f-dbcd-4a09-81f7-9c1d8395d936.jpg"
+ },
+ {
+ "title": "Birthday cake",
+ "desc": "a yearly treat",
+ "image": "http://cornandco.com/wp-content/uploads/2014/05/birthday-cake-popcorn.jpg"
+ }
+]
\ No newline at end of file
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
new file mode 100644
index 00000000..c75d693e
--- /dev/null
+++ b/src/main/resources/logback.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml
index d004447f..f62bd816 100644
--- a/src/main/webapp/WEB-INF/web.xml
+++ b/src/main/webapp/WEB-INF/web.xml
@@ -1,8 +1,7 @@
-
-
-
- Archetype Created Web Application
+
+ Cake Manager
diff --git a/src/main/webapp/css/narrow-jumbotron.css b/src/main/webapp/css/narrow-jumbotron.css
new file mode 100644
index 00000000..3e3cf762
--- /dev/null
+++ b/src/main/webapp/css/narrow-jumbotron.css
@@ -0,0 +1,83 @@
+/* Space out content a bit */
+body {
+ padding-top: 1.5rem;
+ padding-bottom: 1.5rem;
+}
+
+/* Everything but the jumbotron gets side spacing for mobile first views */
+.header,
+.marketing,
+.footer {
+ padding-right: 1rem;
+ padding-left: 1rem;
+}
+
+/* Custom page header */
+.header {
+ padding-bottom: 1rem;
+ border-bottom: .05rem solid #e5e5e5;
+}
+
+/* Make the masthead heading the same height as the navigation */
+.header h3 {
+ margin-top: 0;
+ margin-bottom: 0;
+ line-height: 3rem;
+}
+
+/* Custom page footer */
+.footer {
+ padding-top: 1.5rem;
+ color: #777;
+ border-top: .05rem solid #e5e5e5;
+}
+
+/* Customize container */
+@media (min-width: 48em) {
+ .container {
+ max-width: 46rem;
+ }
+}
+.container-narrow > hr {
+ margin: 2rem 0;
+}
+
+/* Main marketing message and sign up button */
+.jumbotron {
+ text-align: center;
+ border-bottom: .05rem solid #e5e5e5;
+}
+.jumbotron .btn {
+ padding: .75rem 1.5rem;
+ font-size: 1.5rem;
+}
+
+/* Supporting marketing content */
+.marketing {
+ margin: 3rem 0;
+}
+.marketing p + h4 {
+ margin-top: 1.5rem;
+}
+
+/* Responsive: Portrait tablets and up */
+@media screen and (min-width: 48em) {
+ /* Remove the padding we set earlier */
+ .header,
+ .marketing,
+ .footer {
+ padding-right: 0;
+ padding-left: 0;
+ }
+
+ /* Space out the masthead */
+ .header {
+ margin-bottom: 2rem;
+ }
+
+ /* Remove the bottom border on the jumbotron for visual effect */
+ .jumbotron {
+ border-bottom: 0;
+ }
+}
+
diff --git a/src/main/webapp/index.jsp b/src/main/webapp/index.jsp
index c38169bb..bf1b3318 100644
--- a/src/main/webapp/index.jsp
+++ b/src/main/webapp/index.jsp
@@ -1,5 +1,73 @@
-
+<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
+
+
+ Cake Manager
+
+
+
+
+
+
+
+
-Hello World!
+
+
+
+
+
+
+
Cakes currently available in the cake store:
+
+
+
+
+
+
${cake.description}
+
+

+
+
+
+
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/src/test/java/com/waracle/cakemgr/CakeFactoryTest.java b/src/test/java/com/waracle/cakemgr/CakeFactoryTest.java
new file mode 100644
index 00000000..587598ea
--- /dev/null
+++ b/src/test/java/com/waracle/cakemgr/CakeFactoryTest.java
@@ -0,0 +1,57 @@
+package com.waracle.cakemgr;
+
+import static org.junit.Assert.*;
+import com.fasterxml.jackson.core.JsonParser;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+
+import static org.hamcrest.Matchers.is;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class CakeFactoryTest {
+
+ private static final String TEST_TITLE = "test-title";
+ private static final String TEST_DESCRIPTION = "test-description";
+ private static final String TEST_IMAGE = "test-image";
+ private CakeFactory testee;
+ @Mock
+ private JsonParser mockParser;
+ @Mock
+ private HttpServletRequest mockRequest;
+
+ @Before
+ public void setUp() throws Exception {
+ testee = new CakeFactory();
+ when(mockParser.nextFieldName()).thenReturn("title").thenReturn("description").thenReturn("image");
+ when(mockParser.nextTextValue()).thenReturn(TEST_TITLE).thenReturn("test-description").thenReturn("test-image");
+ when(mockRequest.getParameter("title")).thenReturn(TEST_TITLE);
+ when(mockRequest.getParameter("description")).thenReturn(TEST_DESCRIPTION);
+ when(mockRequest.getParameter("image")).thenReturn(TEST_IMAGE);
+ }
+
+ @Test
+ public void cakeTitleIsSetCorrectlyWhenCakeIsMadeFromServletRequest() throws IOException {
+ Cake cake = testee.makeCake(mockRequest);
+ assertThat(cake.getTitle(), is(TEST_TITLE));
+ }
+
+ @Test
+ public void cakeDescriptionIsSetCorrectlyWhenCakeIsMadeFromServletRequest() throws IOException {
+ Cake cake = testee.makeCake(mockRequest);
+ assertThat(cake.getDescription(), is(TEST_DESCRIPTION));
+ }
+
+ @Test
+ public void cakeImageIsSetCorrectlyWhenCakeIsMadeFromServletRequest() throws IOException {
+ Cake cake = testee.makeCake(mockRequest);
+ assertThat(cake.getImage(), is(TEST_IMAGE));
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/com/waracle/cakemgr/CakeServletTest.java b/src/test/java/com/waracle/cakemgr/CakeServletTest.java
new file mode 100644
index 00000000..5a470a12
--- /dev/null
+++ b/src/test/java/com/waracle/cakemgr/CakeServletTest.java
@@ -0,0 +1,109 @@
+package com.waracle.cakemgr;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Arrays;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.*;
+
+
+@RunWith(MockitoJUnitRunner.class)
+public class CakeServletTest {
+
+ private static final String ACCEPT = "accept";
+ private static final String APPLICATION_JSON = "application/json";
+ private CakeServlet testee;
+ @Mock
+ private CakeFactory mockCakeFactory;
+ @Mock
+ private HttpServletRequest mockRequest;
+ @Mock
+ private HttpServletResponse mockResponse;
+ @Mock
+ private CakeStore mockStore;
+ @Mock
+ private PrintWriter mockWriter;
+ @Mock
+ private ServletContext mockServletContext;
+ @Mock
+ private RequestDispatcher mockDispatcher;
+
+
+ @Before
+ public void setUp() throws IOException {
+ testee = new TestCakeServlet(mockCakeFactory, mockStore);
+ when(mockResponse.getWriter()).thenReturn(mockWriter);
+ Cake fruitCake = new Cake();
+ fruitCake.setTitle("Fruit Cake");
+ fruitCake.setDescription("Full of fruity goodness");
+ fruitCake.setImage("http://fruit-cake-pictures/pic1.jpeg");
+ when(mockStore.allCakes()).thenReturn(Arrays.asList(fruitCake));
+ when(mockServletContext.getRequestDispatcher("/index.jsp")).thenReturn(mockDispatcher);
+ }
+
+ @Test
+ public void cakesAreStoredToDatabaseDuringInitialisation() throws ServletException, IOException {
+ testee.init();
+ verify(mockStore, times(20)).store(any(Cake.class));
+ }
+
+ @Test
+ public void jsonIsReturnedWhenDoGetIsCalledAndAcceptHeaderIsAppJson() throws ServletException, IOException {
+ when(mockRequest.getHeader(ACCEPT)).thenReturn(APPLICATION_JSON);
+ testee.doGet(mockRequest, mockResponse);
+ verify(mockWriter).print("[{\"cakeId\":null,\"title\":\"Fruit Cake\",\"image\":\"http://fruit-cake-pictures/pic1.jpeg\",\"desc\":\"Full of fruity goodness\"}]");
+ }
+
+ @Test
+ public void htmlIsReturnedWhenDoGetIsCalledAndAcceptHeaderIsTextHTML() throws ServletException, IOException {
+ when(mockRequest.getHeader(ACCEPT)).thenReturn("text/html");
+ testee.doGet(mockRequest, mockResponse);
+ verify(mockDispatcher).forward(mockRequest, mockResponse);
+ }
+
+ @Test
+ public void cakeIsMadeWhenPosted() throws ServletException, IOException {
+ when(mockRequest.getHeader(ACCEPT)).thenReturn(APPLICATION_JSON);
+ testee.doPost(mockRequest, mockResponse);
+ verify(mockCakeFactory).makeCake(mockRequest);
+ }
+
+ @Test
+ public void cakeIsStoredWhenPosted() throws ServletException, IOException {
+ when(mockRequest.getHeader(ACCEPT)).thenReturn(APPLICATION_JSON);
+ testee.doPost(mockRequest, mockResponse);
+ verify(mockStore).store(any(Cake.class));
+ }
+
+ @Test
+ public void jsonIsReturnedWhenCakePosted() throws ServletException, IOException {
+ when(mockRequest.getHeader(ACCEPT)).thenReturn(APPLICATION_JSON);
+ testee.doPost(mockRequest, mockResponse);
+ verify(mockWriter).print(anyString());
+ }
+
+ /**
+ ** private wrapper class used to inject mock servletContext
+ **/
+ private class TestCakeServlet extends CakeServlet {
+ TestCakeServlet(CakeFactory cakeFactory, CakeStore cakeStore) {
+ super(cakeFactory, cakeStore);
+ }
+ public ServletContext getServletContext() {
+ return mockServletContext;
+ }
+ }
+}
diff --git a/src/test/java/com/waracle/cakemgr/CakeStoreTest.java b/src/test/java/com/waracle/cakemgr/CakeStoreTest.java
new file mode 100644
index 00000000..e1eec435
--- /dev/null
+++ b/src/test/java/com/waracle/cakemgr/CakeStoreTest.java
@@ -0,0 +1,71 @@
+package com.waracle.cakemgr;
+
+import static org.junit.Assert.*;
+import org.hibernate.Criteria;
+import org.hibernate.Session;
+import org.hibernate.Transaction;
+import org.hibernate.criterion.Criterion;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import static org.hamcrest.core.Is.is;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class CakeStoreTest {
+
+ private CakeStore testee;
+ @Mock
+ private Criteria mockCriteria;
+ @Mock
+ private Transaction mockTransaction;
+ @Mock
+ private Session mockSession;
+ @Captor
+ private ArgumentCaptor cakeArgument;
+ @Mock
+ private CakeFactory mockCakeFactory;
+
+ @Before
+ public void setUp() {
+ testee = new CakeStore();
+ when(mockSession.createCriteria(Cake.class)).thenReturn(mockCriteria);
+ when(mockCriteria.add(any(Criterion.class))).thenReturn(mockCriteria);
+ when(mockSession.getTransaction()).thenReturn(mockTransaction);
+ }
+
+ @Test
+ public void cakeIsStoredCorrectlyWhenNotDuplicate() {
+ Cake cake = new Cake();
+ when(mockCriteria.list()).thenReturn(new ArrayList());
+ testee.store(cake, mockSession);
+ verify(mockTransaction).commit();
+ verify(mockSession).close();
+ }
+
+ @Test
+ public void cakeDescriptionIsUpdatedWhenDuplicate() {
+ Cake cake1 = new Cake();
+ cake1.setTitle("Walnut Cake");
+ when(mockCriteria.list()).thenReturn(Arrays.asList(cake1));
+ Cake cake2 = new Cake();
+ cake2.setTitle("Walnut Cake");
+ cake2.setDescription("updated description");
+ testee.store(cake2, mockSession);
+ verify(mockTransaction).commit();
+ verify(mockSession).close();
+ verify(mockSession).persist(cakeArgument.capture());
+ Cake cake3 = cakeArgument.getValue();
+ assertThat(cake3.getDescription(), is("updated description"));
+ }
+}