diff --git a/README.md b/README.md index 95c6628..3c3b2c4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Spark MVC Tutorial =================== -See the entire walkthrough on my [blog](http://taywils.me/2013/11/05/javasparkframeworktutorial.html) +See the entire walkthrough on my [Java Spark Framework Tutorial | Taywils.me](http://taywils.me/2013/11/05/javasparkframeworktutorial/) ## Instructions diff --git a/pom.xml b/pom.xml index c15c0a4..d2bb18f 100644 --- a/pom.xml +++ b/pom.xml @@ -8,5 +8,29 @@ sparkle 1.0-SNAPSHOT - + + + com.sparkjava + spark-core + 1.1.1 + + + + com.sparkjava + spark-template-freemarker + 1.0 + + + + postgresql + postgresql + 9.1-901.jdbc4 + + + + org.mongodb + mongo-java-driver + 2.11.3 + + \ No newline at end of file diff --git a/src/main/java/Article.java b/src/main/java/Article.java new file mode 100644 index 0000000..0f5d5b4 --- /dev/null +++ b/src/main/java/Article.java @@ -0,0 +1,83 @@ +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class Article { + private String title; + private String content; + private Date createdAt; + private String summary; + private Integer id; + private Boolean deleted; + + public Article(String title, String summary, String content, Integer size) { + this.title = title; + this.summary = summary; + this.content = content; + this.createdAt = new Date(); + this.id = size; + this.deleted = false; + } + + public Article(String title, String summary, String content, Integer id, Date createdAt, Boolean deleted) { + this.title = title; + this.summary = summary; + this.content = content; + this.createdAt = createdAt; + this.id = id; + this.deleted = deleted; + } + + public String getTitle() { + return title; + } + + public String getContent() { + return content; + } + + public String getSummary() { + return summary; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setContent(String content) { + this.content = content; + } + + public void setSummary(String summary) { + this.summary = summary; + } + + public Integer getId() { + return id; + } + + public void delete() { + this.deleted = true; + } + + public Boolean readable() { + return !this.deleted; + } + + public String getCreatedAt() { + DateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy"); + return dateFormat.format(this.createdAt); + } + + public String getEditLink() { + return "Edit"; + } + + public String getDeleteLink() { + return "Delete"; + } + + public String getSummaryLink() { + return "" + this.summary + ""; + } +} diff --git a/src/main/java/ArticleDbService.java b/src/main/java/ArticleDbService.java new file mode 100644 index 0000000..dbf0cef --- /dev/null +++ b/src/main/java/ArticleDbService.java @@ -0,0 +1,9 @@ +import java.util.ArrayList; + +public interface ArticleDbService { + public Boolean create(T entity); + public T readOne(int id); + public ArrayList readAll(); + public Boolean update(int id, String title, String summary, String content); + public Boolean delete(int id); +} diff --git a/src/main/java/ArticleMongoDao.java b/src/main/java/ArticleMongoDao.java new file mode 100644 index 0000000..11c2d3c --- /dev/null +++ b/src/main/java/ArticleMongoDao.java @@ -0,0 +1,160 @@ +import com.mongodb.*; + +import java.sql.Date; +import java.util.ArrayList; + +public class ArticleMongoDao implements ArticleDbService { + // A collection in Mongo can be thought of as a table in a relational DB + private DBCollection collection; + + public ArticleMongoDao() { + + try { + // Connect to MongoDB using the default port on your local machine + MongoClient mongoClient = new MongoClient("localhost"); + // Note that the sparkledb will not actually be created until we save a document + DB db = mongoClient.getDB("sparkledb"); + collection = db.getCollection("Articles"); + + System.out.println("Connecting to MongoDB@" + mongoClient.getAllAddress()); + } catch(Exception e) { + System.out.println(e.getMessage()); + } + } + + @Override + public Boolean create(T entity) { + // MongoDB is a document store which by default has no concept of a schema so its + // entirely up to the developer to decide which attributes a document will use + BasicDBObject doc = new BasicDBObject("title", entity.getTitle()). + append("id", entity.getId()). + append("content", entity.getContent()). + append("summary", entity.getSummary()). + append("deleted", false). + append("createdAt", new Date(new java.util.Date().getTime())); + + // As soon as we insert a doucment into our collection MongoDB will craete our sparkle database and + // Article collection within it. + collection.insert(doc); + return true; + } + + @Override + @SuppressWarnings("unchecked") + public T readOne(int id) { + // MongoDB queries are not queries in the sense that you have a separate language such as SQL + // With MongoDB you can think of it as Document pattern matching + // Thus we construct a document with a specific id value and ask Mongo to search our Article + // collection for all documents which match + BasicDBObject query = new BasicDBObject("id", id); + + // Cursors are the default representation of Mongo query result + // Think of cursors as a pointer to a array of documents + // It can traverse the array of documents and when requested can dereference and pull out the contents + // But at any given time it only takes up enough memory needed to maintain the reference of the data type it points to + // MongoDB was written in C++ so an analogy to the C language is probably how Cursors were implemented + + // A technical presentation by Dwight Merriman co-founder of 10gen the company that makes MongoDB + // @see http://www.mongodb.com/presentations/mongodb-internals-tour-source-code + // Watch that shit... best technical deep-dive of MongoDB ever! + DBCursor cursor = collection.find(query); + + try { + if(cursor.hasNext()) { + BasicDBObject doc = (BasicDBObject) cursor.next(); + Article entity = new Article( + doc.getString("title"), + doc.getString("summary"), + doc.getString("content"), + doc.getInt("id"), + doc.getDate("createdAt"), + doc.getBoolean("deleted") + ); + + return (T) entity; + } else { + return null; + } + } finally { + cursor.close(); + } + } + + @Override + @SuppressWarnings("unchecked") + public ArrayList readAll() { + // When you use DBCollection::find() without an argument it defaults to find all + DBCursor cursor = collection.find(); + + ArrayList
results = (ArrayList
) new ArrayList(); + + try { + while(cursor.hasNext()) { + BasicDBObject doc = (BasicDBObject) cursor.next(); + + Article entity = new Article( + doc.getString("title"), + doc.getString("summary"), + doc.getString("content"), + doc.getInt("id"), + doc.getDate("createdAt"), + doc.getBoolean("deleted") + ); + + results.add(entity); + } + + return (ArrayList) results; + } finally { + cursor.close(); + } + } + + @Override + public Boolean update(int id, String title, String summary, String content) { + // NOTE: MongoDB also allow us to do SQL style updates by specifying update conditions + // within our query document. It requires a much deeper knowledge of MongoDB but for now + // we can stick with the less performant(two operations versus one) find() and put() style of updating + BasicDBObject query = new BasicDBObject("id", id); + + DBCursor cursor = collection.find(query); + + try { + if(cursor.hasNext()) { + BasicDBObject doc = (BasicDBObject) cursor.next(); + // BasicDBObject::put() allows us to update a document in-place + doc.put("title", title); + doc.put("summary", summary); + doc.put("content", content); + + collection.save(doc); + + return true; + } else { + return false; + } + } finally { + cursor.close(); + } + } + + @Override + public Boolean delete(int id) { + BasicDBObject query = new BasicDBObject("id", id); + + DBCursor cursor = collection.find(query); + + try { + if(cursor.hasNext()) { + // Deleting works by telling the cursor to free the document currently being pointed at + collection.remove(cursor.next()); + + return true; + } else { + return false; + } + } finally { + cursor.close(); + } + } +} diff --git a/src/main/java/ArticlePostgresDao.java b/src/main/java/ArticlePostgresDao.java new file mode 100644 index 0000000..bca4657 --- /dev/null +++ b/src/main/java/ArticlePostgresDao.java @@ -0,0 +1,246 @@ +import java.sql.*; +import java.util.ArrayList; + +public class ArticlePostgresDao implements ArticleDbService { + // PostgreSQL connection to the database + private Connection conn; + // A raw SQL query used without parameters + private Statement stmt; + + public ArticlePostgresDao() { + // The account names setup from the command line interface + String user = "postgres"; + String passwd = "postgres"; + String dbName = "sparkledb"; + // DB connection on localhost via JDBC + String uri = "jdbc:postgresql://localhost/" + dbName; + + // Standard SQL CREATE TABLE query + // The primary key is not auto incremented + String createTableQuery = + "CREATE TABLE IF NOT EXISTS article(" + + "id INT PRIMARY KEY NOT NULL," + + "title VARCHAR(64) NOT NULL," + + "content VARCHAR(512)NOT NULL," + + "summary VARCHAR(64) NOT NULL," + + "deleted BOOLEAN DEFAULT FALSE," + + "createdAt DATE NOT NULL" + + ");" + ; + + // Create the article table within sparkledb and close resources if an exception is thrown + try { + conn = DriverManager.getConnection(uri, user, passwd); + stmt = conn.createStatement(); + stmt.execute(createTableQuery); + + System.out.println("Connecting to PostgreSQL database"); + } catch(Exception e) { + System.out.println(e.getMessage()); + + try { + if(null != stmt) { + stmt.close(); + } + if(null != conn) { + conn.close(); + } + } catch (SQLException sqlException) { + sqlException.printStackTrace(); + } + } + } + + @Override + public Boolean create(T entity) { + try { + String insertQuery = "INSERT INTO article(id, title, content, summary, createdAt) VALUES(?, ?, ?, ?, ?);"; + + // Prepared statements allow us to avoid SQL injection attacks + PreparedStatement pstmt = conn.prepareStatement(insertQuery); + + // JDBC binds every prepared statement argument to a Java Class such as Integer and or String + pstmt.setInt(1, entity.getId()); + pstmt.setString(2, entity.getTitle()); + pstmt.setString(3, entity.getContent()); + pstmt.setString(4, entity.getSummary()); + + java.sql.Date sqlNow = new Date(new java.util.Date().getTime()); + pstmt.setDate(5, sqlNow); + + pstmt.executeUpdate(); + + // Unless closed prepared statement connections will linger + // Not very important for a trivial app but it will burn you in a professional large codebase + pstmt.close(); + + return true; + } catch (SQLException e) { + System.out.println(e.getMessage()); + + try { + if(null != stmt) { + stmt.close(); + } + if(null != conn) { + conn.close(); + } + } catch (SQLException sqlException) { + sqlException.printStackTrace(); + } + + return false; + } + } + + @Override + @SuppressWarnings("unchecked") + public T readOne(int id) { + try { + String selectQuery = "SELECT * FROM article where id = ?"; + + PreparedStatement pstmt = conn.prepareStatement(selectQuery); + pstmt.setInt(1, id); + + pstmt.executeQuery(); + + // A ResultSet is Class which represents a table returned by a SQL query + ResultSet resultSet = pstmt.getResultSet(); + + if(resultSet.next()) { + Article entity = new Article( + // You must know both the column name and the type to extract the row + resultSet.getString("title"), + resultSet.getString("summary"), + resultSet.getString("content"), + resultSet.getInt("id"), + resultSet.getDate("createdat"), + resultSet.getBoolean("deleted") + ); + + pstmt.close(); + + return (T) entity; + } + } catch(Exception e) { + System.out.println(e.getMessage()); + + try { + if(null != stmt) { + stmt.close(); + } + if(null != conn) { + conn.close(); + } + } catch (SQLException sqlException) { + sqlException.printStackTrace(); + } + } + + return null; + } + + @Override + @SuppressWarnings("unchecked") //Tells the compiler to ignore unchecked type casts + public ArrayList readAll() { + // Type cast the generic T into an Article + ArrayList
results = (ArrayList
) new ArrayList(); + + try { + String query = "SELECT * FROM article;"; + + stmt.execute(query); + ResultSet resultSet = stmt.getResultSet(); + + while(resultSet.next()) { + Article entity = new Article( + resultSet.getString("title"), + resultSet.getString("summary"), + resultSet.getString("content"), + resultSet.getInt("id"), + resultSet.getDate("createdat"), + resultSet.getBoolean("deleted") + ); + + results.add(entity); + } + } catch(Exception e) { + System.out.println(e.getMessage()); + + try { + if(null != stmt) { + stmt.close(); + } + if(null != conn) { + conn.close(); + } + } catch (SQLException sqlException) { + sqlException.printStackTrace(); + } + } + + // The interface ArticleDbService relies upon the generic type T so we cast it back + return (ArrayList) results; + } + + @Override + public Boolean update(int id, String title, String summary, String content) { + try { + String updateQuery = + "UPDATE article SET title = ?, summary = ?, content = ?" + + "WHERE id = ?;" + ; + + PreparedStatement pstmt = conn.prepareStatement(updateQuery); + + pstmt.setString(1, title); + pstmt.setString(2, summary); + pstmt.setString(3, content); + pstmt.setInt(4, id); + + pstmt.executeUpdate(); + } catch(Exception e) { + System.out.println(e.getMessage()); + + try { + if(null != stmt) { + stmt.close(); + } + if(null != conn) { + conn.close(); + } + } catch (SQLException sqlException) { + sqlException.printStackTrace(); + } + } + + return true; + } + + @Override + public Boolean delete(int id) { + try { + String deleteQuery = "DELETE FROM article WHERE id = ?"; + + PreparedStatement pstmt = conn.prepareStatement(deleteQuery); + pstmt.setInt(1, id); + + pstmt.executeUpdate(); + } catch (Exception e) { + System.out.println(e.getMessage()); + + try { + if(null != stmt) { + stmt.close(); + } + if(null != conn) { + conn.close(); + } + } catch (SQLException sqlException) { + sqlException.printStackTrace(); + } + } + + return true; + } +} diff --git a/src/main/java/ArticleServletDao.java b/src/main/java/ArticleServletDao.java new file mode 100644 index 0000000..4489649 --- /dev/null +++ b/src/main/java/ArticleServletDao.java @@ -0,0 +1,42 @@ +import java.util.ArrayList; + +public class ArticleServletDao implements ArticleDbService { + ArrayList storage; + + public ArticleServletDao() { + storage = new ArrayList(); + } + + @Override + public Boolean create(T entity) { + storage.add(entity); + return null; + } + + @Override + public T readOne(int id) { + return storage.get(id); + } + + @Override + public ArrayList readAll() { + return storage; + } + + @Override + public Boolean update(int id, String title, String summary, String content) { + T entity = storage.get(id); + + entity.setSummary(summary); + entity.setTitle(title); + entity.setContent(content); + + return true; + } + + @Override + public Boolean delete(int id) { + storage.get(id).delete(); + return true; + } +} diff --git a/src/main/java/HelloSpark.java b/src/main/java/HelloSpark.java index bbbf329..b8da2a3 100644 --- a/src/main/java/HelloSpark.java +++ b/src/main/java/HelloSpark.java @@ -1,5 +1,126 @@ +import static spark.Spark.*; + +import spark.ModelAndView; +import spark.Request; +import spark.Response; +import spark.Route; +import spark.template.freemarker.FreeMarkerRoute; + +import java.util.*; + public class HelloSpark { + public static ArticleDbService
articleDbService = new ArticleServletDao
(); + public static void main(String[] args) { - System.out.println("Hello World!"); + get(new FreeMarkerRoute("/") { + @Override + public ModelAndView handle(Request request, Response response) { + Map viewObjects = new HashMap(); + ArrayList
articles = articleDbService.readAll(); + + if (articles.isEmpty()) { + viewObjects.put("hasNoArticles", "Welcome, please click \"Write Article\" to begin."); + } else { + Deque
showArticles = new ArrayDeque
(); + + for (Article article : articles) { + if (article.readable()) { + showArticles.addFirst(article); + } + } + + viewObjects.put("articles", showArticles); + } + + viewObjects.put("templateName", "articleList.ftl"); + + return modelAndView(viewObjects, "layout.ftl"); + } + }); + + get(new FreeMarkerRoute("/article/create") { + @Override + public Object handle(Request request, Response response) { + Map viewObjects = new HashMap(); + + viewObjects.put("templateName", "articleForm.ftl"); + + return modelAndView(viewObjects, "layout.ftl"); + } + }); + + post(new Route("/article/create") { + @Override + public Object handle(Request request, Response response) { + String title = request.queryParams("article-title"); + String summary = request.queryParams("article-summary"); + String content = request.queryParams("article-content"); + + Article article = new Article(title, summary, content, articleDbService.readAll().size()); + + articleDbService.create(article); + + response.status(201); + response.redirect("/"); + return ""; + } + }); + + get(new FreeMarkerRoute("/article/read/:id") { + @Override + public Object handle(Request request, Response response) { + Integer id = Integer.parseInt(request.params(":id")); + Map viewObjects = new HashMap(); + + viewObjects.put("templateName", "articleRead.ftl"); + + viewObjects.put("article", articleDbService.readOne(id)); + + return modelAndView(viewObjects, "layout.ftl"); + } + }); + + get(new FreeMarkerRoute("/article/update/:id") { + @Override + public Object handle(Request request, Response response) { + Integer id = Integer.parseInt(request.params(":id")); + Map viewObjects = new HashMap(); + + viewObjects.put("templateName", "articleForm.ftl"); + + viewObjects.put("article", articleDbService.readOne(id)); + + return modelAndView(viewObjects, "layout.ftl"); + } + }); + + post(new Route("/article/update/:id") { + @Override + public Object handle(Request request, Response response) { + Integer id = Integer.parseInt(request.queryParams("article-id")); + String title = request.queryParams("article-title"); + String summary = request.queryParams("article-summary"); + String content = request.queryParams("article-content"); + + articleDbService.update(id, title, summary, content); + + response.status(200); + response.redirect("/"); + return ""; + } + }); + + get(new Route("/article/delete/:id") { + @Override + public Object handle(Request request, Response response) { + Integer id = Integer.parseInt(request.params(":id")); + + articleDbService.delete(id); + + response.status(200); + response.redirect("/"); + return ""; + } + }); } } \ No newline at end of file diff --git a/src/main/resources/spark/template/freemarker/articleForm.ftl b/src/main/resources/spark/template/freemarker/articleForm.ftl new file mode 100644 index 0000000..557eff0 --- /dev/null +++ b/src/main/resources/spark/template/freemarker/articleForm.ftl @@ -0,0 +1,23 @@ +
+
action="/article/update/:id"<#else>action="/article/create"> +
+ +
+ value="${article.getTitle()}" /> +
+
+
+ +
+ value="${article.getSummary()}" /> +
+
+ <#if article??> + + +
+ + + + value='Update'<#else>value='Publish' class="btn btn-primary" form='article-create-form' /> +
\ No newline at end of file diff --git a/src/main/resources/spark/template/freemarker/articleList.ftl b/src/main/resources/spark/template/freemarker/articleList.ftl new file mode 100644 index 0000000..129e3e5 --- /dev/null +++ b/src/main/resources/spark/template/freemarker/articleList.ftl @@ -0,0 +1,14 @@ +<#if hasNoArticles??> +
+

${hasNoArticles}

+
+<#else> +
+ <#list articles as article> +

${article.getTitle()}

+

${article.getCreatedAt()}

+

${article.getSummaryLink()}

+

${article.getEditLink()} | ${article.getDeleteLink()}

+ +
+ \ No newline at end of file diff --git a/src/main/resources/spark/template/freemarker/articleRead.ftl b/src/main/resources/spark/template/freemarker/articleRead.ftl new file mode 100644 index 0000000..cae3938 --- /dev/null +++ b/src/main/resources/spark/template/freemarker/articleRead.ftl @@ -0,0 +1,6 @@ +
+

${article.getTitle()}

+

${article.getCreatedAt()}

+

${article.getEditLink()} | ${article.getDeleteLink()}

+
${article.getContent()}
+
\ No newline at end of file diff --git a/src/main/resources/spark/template/freemarker/layout.ftl b/src/main/resources/spark/template/freemarker/layout.ftl new file mode 100644 index 0000000..199cc1c --- /dev/null +++ b/src/main/resources/spark/template/freemarker/layout.ftl @@ -0,0 +1,36 @@ + + + Spark Blog + + + + + + + + +
+ <#include "${templateName}"> +
+ + + + +