├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src └── main ├── java ├── Article.java ├── ArticleDbService.java ├── ArticleMongoDao.java ├── ArticlePostgresDao.java ├── ArticleServletDao.java └── HelloSpark.java └── resources └── spark └── template └── freemarker ├── articleForm.ftl ├── articleList.ftl ├── articleRead.ftl └── layout.ftl /.gitignore: -------------------------------------------------------------------------------- 1 | # Java 2 | *.class 3 | *.jar 4 | *.war 5 | *.ear 6 | 7 | # IntelliJ 8 | *.iml 9 | *.ipr 10 | *.iws 11 | .idea/ 12 | out/ 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 taywils 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Spark MVC Tutorial 2 | =================== 3 | 4 | See the entire walkthrough on my [Java Spark Framework Tutorial | Taywils.me](http://taywils.me/2013/11/05/javasparkframeworktutorial/) 5 | 6 | ## Instructions 7 | 8 | - For each of the steps outlined on the blog post just _git checkout_ the corresponding branch name 9 | 10 | - Or just follow along and read through the blog article 11 | 12 | You're welcome 13 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | sparkle 8 | sparkle 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 13 | com.sparkjava 14 | spark-core 15 | 1.1.1 16 | 17 | 18 | 19 | com.sparkjava 20 | spark-template-freemarker 21 | 1.0 22 | 23 | 24 | 25 | postgresql 26 | postgresql 27 | 9.1-901.jdbc4 28 | 29 | 30 | 31 | org.mongodb 32 | mongo-java-driver 33 | 2.11.3 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/main/java/Article.java: -------------------------------------------------------------------------------- 1 | import java.text.DateFormat; 2 | import java.text.SimpleDateFormat; 3 | import java.util.Date; 4 | 5 | public class Article { 6 | private String title; 7 | private String content; 8 | private Date createdAt; 9 | private String summary; 10 | private Integer id; 11 | private Boolean deleted; 12 | 13 | public Article(String title, String summary, String content, Integer size) { 14 | this.title = title; 15 | this.summary = summary; 16 | this.content = content; 17 | this.createdAt = new Date(); 18 | this.id = size; 19 | this.deleted = false; 20 | } 21 | 22 | public Article(String title, String summary, String content, Integer id, Date createdAt, Boolean deleted) { 23 | this.title = title; 24 | this.summary = summary; 25 | this.content = content; 26 | this.createdAt = createdAt; 27 | this.id = id; 28 | this.deleted = deleted; 29 | } 30 | 31 | public String getTitle() { 32 | return title; 33 | } 34 | 35 | public String getContent() { 36 | return content; 37 | } 38 | 39 | public String getSummary() { 40 | return summary; 41 | } 42 | 43 | public void setTitle(String title) { 44 | this.title = title; 45 | } 46 | 47 | public void setContent(String content) { 48 | this.content = content; 49 | } 50 | 51 | public void setSummary(String summary) { 52 | this.summary = summary; 53 | } 54 | 55 | public Integer getId() { 56 | return id; 57 | } 58 | 59 | public void delete() { 60 | this.deleted = true; 61 | } 62 | 63 | public Boolean readable() { 64 | return !this.deleted; 65 | } 66 | 67 | public String getCreatedAt() { 68 | DateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy"); 69 | return dateFormat.format(this.createdAt); 70 | } 71 | 72 | public String getEditLink() { 73 | return "Edit"; 74 | } 75 | 76 | public String getDeleteLink() { 77 | return "Delete"; 78 | } 79 | 80 | public String getSummaryLink() { 81 | return "" + this.summary + ""; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/ArticleDbService.java: -------------------------------------------------------------------------------- 1 | import java.util.ArrayList; 2 | 3 | public interface ArticleDbService { 4 | public Boolean create(T entity); 5 | public T readOne(int id); 6 | public ArrayList readAll(); 7 | public Boolean update(int id, String title, String summary, String content); 8 | public Boolean delete(int id); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/ArticleMongoDao.java: -------------------------------------------------------------------------------- 1 | import com.mongodb.*; 2 | 3 | import java.sql.Date; 4 | import java.util.ArrayList; 5 | 6 | public class ArticleMongoDao implements ArticleDbService { 7 | // A collection in Mongo can be thought of as a table in a relational DB 8 | private DBCollection collection; 9 | 10 | public ArticleMongoDao() { 11 | 12 | try { 13 | // Connect to MongoDB using the default port on your local machine 14 | MongoClient mongoClient = new MongoClient("localhost"); 15 | // Note that the sparkledb will not actually be created until we save a document 16 | DB db = mongoClient.getDB("sparkledb"); 17 | collection = db.getCollection("Articles"); 18 | 19 | System.out.println("Connecting to MongoDB@" + mongoClient.getAllAddress()); 20 | } catch(Exception e) { 21 | System.out.println(e.getMessage()); 22 | } 23 | } 24 | 25 | @Override 26 | public Boolean create(T entity) { 27 | // MongoDB is a document store which by default has no concept of a schema so its 28 | // entirely up to the developer to decide which attributes a document will use 29 | BasicDBObject doc = new BasicDBObject("title", entity.getTitle()). 30 | append("id", entity.getId()). 31 | append("content", entity.getContent()). 32 | append("summary", entity.getSummary()). 33 | append("deleted", false). 34 | append("createdAt", new Date(new java.util.Date().getTime())); 35 | 36 | // As soon as we insert a doucment into our collection MongoDB will craete our sparkle database and 37 | // Article collection within it. 38 | collection.insert(doc); 39 | return true; 40 | } 41 | 42 | @Override 43 | @SuppressWarnings("unchecked") 44 | public T readOne(int id) { 45 | // MongoDB queries are not queries in the sense that you have a separate language such as SQL 46 | // With MongoDB you can think of it as Document pattern matching 47 | // Thus we construct a document with a specific id value and ask Mongo to search our Article 48 | // collection for all documents which match 49 | BasicDBObject query = new BasicDBObject("id", id); 50 | 51 | // Cursors are the default representation of Mongo query result 52 | // Think of cursors as a pointer to a array of documents 53 | // It can traverse the array of documents and when requested can dereference and pull out the contents 54 | // But at any given time it only takes up enough memory needed to maintain the reference of the data type it points to 55 | // MongoDB was written in C++ so an analogy to the C language is probably how Cursors were implemented 56 | 57 | // A technical presentation by Dwight Merriman co-founder of 10gen the company that makes MongoDB 58 | // @see http://www.mongodb.com/presentations/mongodb-internals-tour-source-code 59 | // Watch that shit... best technical deep-dive of MongoDB ever! 60 | DBCursor cursor = collection.find(query); 61 | 62 | try { 63 | if(cursor.hasNext()) { 64 | BasicDBObject doc = (BasicDBObject) cursor.next(); 65 | Article entity = new Article( 66 | doc.getString("title"), 67 | doc.getString("summary"), 68 | doc.getString("content"), 69 | doc.getInt("id"), 70 | doc.getDate("createdAt"), 71 | doc.getBoolean("deleted") 72 | ); 73 | 74 | return (T) entity; 75 | } else { 76 | return null; 77 | } 78 | } finally { 79 | cursor.close(); 80 | } 81 | } 82 | 83 | @Override 84 | @SuppressWarnings("unchecked") 85 | public ArrayList readAll() { 86 | // When you use DBCollection::find() without an argument it defaults to find all 87 | DBCursor cursor = collection.find(); 88 | 89 | ArrayList
results = (ArrayList
) new ArrayList(); 90 | 91 | try { 92 | while(cursor.hasNext()) { 93 | BasicDBObject doc = (BasicDBObject) cursor.next(); 94 | 95 | Article entity = new Article( 96 | doc.getString("title"), 97 | doc.getString("summary"), 98 | doc.getString("content"), 99 | doc.getInt("id"), 100 | doc.getDate("createdAt"), 101 | doc.getBoolean("deleted") 102 | ); 103 | 104 | results.add(entity); 105 | } 106 | 107 | return (ArrayList) results; 108 | } finally { 109 | cursor.close(); 110 | } 111 | } 112 | 113 | @Override 114 | public Boolean update(int id, String title, String summary, String content) { 115 | // NOTE: MongoDB also allow us to do SQL style updates by specifying update conditions 116 | // within our query document. It requires a much deeper knowledge of MongoDB but for now 117 | // we can stick with the less performant(two operations versus one) find() and put() style of updating 118 | BasicDBObject query = new BasicDBObject("id", id); 119 | 120 | DBCursor cursor = collection.find(query); 121 | 122 | try { 123 | if(cursor.hasNext()) { 124 | BasicDBObject doc = (BasicDBObject) cursor.next(); 125 | // BasicDBObject::put() allows us to update a document in-place 126 | doc.put("title", title); 127 | doc.put("summary", summary); 128 | doc.put("content", content); 129 | 130 | collection.save(doc); 131 | 132 | return true; 133 | } else { 134 | return false; 135 | } 136 | } finally { 137 | cursor.close(); 138 | } 139 | } 140 | 141 | @Override 142 | public Boolean delete(int id) { 143 | BasicDBObject query = new BasicDBObject("id", id); 144 | 145 | DBCursor cursor = collection.find(query); 146 | 147 | try { 148 | if(cursor.hasNext()) { 149 | // Deleting works by telling the cursor to free the document currently being pointed at 150 | collection.remove(cursor.next()); 151 | 152 | return true; 153 | } else { 154 | return false; 155 | } 156 | } finally { 157 | cursor.close(); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/main/java/ArticlePostgresDao.java: -------------------------------------------------------------------------------- 1 | import java.sql.*; 2 | import java.util.ArrayList; 3 | 4 | public class ArticlePostgresDao implements ArticleDbService { 5 | // PostgreSQL connection to the database 6 | private Connection conn; 7 | // A raw SQL query used without parameters 8 | private Statement stmt; 9 | 10 | public ArticlePostgresDao() { 11 | // The account names setup from the command line interface 12 | String user = "postgres"; 13 | String passwd = "postgres"; 14 | String dbName = "sparkledb"; 15 | // DB connection on localhost via JDBC 16 | String uri = "jdbc:postgresql://localhost/" + dbName; 17 | 18 | // Standard SQL CREATE TABLE query 19 | // The primary key is not auto incremented 20 | String createTableQuery = 21 | "CREATE TABLE IF NOT EXISTS article(" + 22 | "id INT PRIMARY KEY NOT NULL," + 23 | "title VARCHAR(64) NOT NULL," + 24 | "content VARCHAR(512)NOT NULL," + 25 | "summary VARCHAR(64) NOT NULL," + 26 | "deleted BOOLEAN DEFAULT FALSE," + 27 | "createdAt DATE NOT NULL" + 28 | ");" 29 | ; 30 | 31 | // Create the article table within sparkledb and close resources if an exception is thrown 32 | try { 33 | conn = DriverManager.getConnection(uri, user, passwd); 34 | stmt = conn.createStatement(); 35 | stmt.execute(createTableQuery); 36 | 37 | System.out.println("Connecting to PostgreSQL database"); 38 | } catch(Exception e) { 39 | System.out.println(e.getMessage()); 40 | 41 | try { 42 | if(null != stmt) { 43 | stmt.close(); 44 | } 45 | if(null != conn) { 46 | conn.close(); 47 | } 48 | } catch (SQLException sqlException) { 49 | sqlException.printStackTrace(); 50 | } 51 | } 52 | } 53 | 54 | @Override 55 | public Boolean create(T entity) { 56 | try { 57 | String insertQuery = "INSERT INTO article(id, title, content, summary, createdAt) VALUES(?, ?, ?, ?, ?);"; 58 | 59 | // Prepared statements allow us to avoid SQL injection attacks 60 | PreparedStatement pstmt = conn.prepareStatement(insertQuery); 61 | 62 | // JDBC binds every prepared statement argument to a Java Class such as Integer and or String 63 | pstmt.setInt(1, entity.getId()); 64 | pstmt.setString(2, entity.getTitle()); 65 | pstmt.setString(3, entity.getContent()); 66 | pstmt.setString(4, entity.getSummary()); 67 | 68 | java.sql.Date sqlNow = new Date(new java.util.Date().getTime()); 69 | pstmt.setDate(5, sqlNow); 70 | 71 | pstmt.executeUpdate(); 72 | 73 | // Unless closed prepared statement connections will linger 74 | // Not very important for a trivial app but it will burn you in a professional large codebase 75 | pstmt.close(); 76 | 77 | return true; 78 | } catch (SQLException e) { 79 | System.out.println(e.getMessage()); 80 | 81 | try { 82 | if(null != stmt) { 83 | stmt.close(); 84 | } 85 | if(null != conn) { 86 | conn.close(); 87 | } 88 | } catch (SQLException sqlException) { 89 | sqlException.printStackTrace(); 90 | } 91 | 92 | return false; 93 | } 94 | } 95 | 96 | @Override 97 | @SuppressWarnings("unchecked") 98 | public T readOne(int id) { 99 | try { 100 | String selectQuery = "SELECT * FROM article where id = ?"; 101 | 102 | PreparedStatement pstmt = conn.prepareStatement(selectQuery); 103 | pstmt.setInt(1, id); 104 | 105 | pstmt.executeQuery(); 106 | 107 | // A ResultSet is Class which represents a table returned by a SQL query 108 | ResultSet resultSet = pstmt.getResultSet(); 109 | 110 | if(resultSet.next()) { 111 | Article entity = new Article( 112 | // You must know both the column name and the type to extract the row 113 | resultSet.getString("title"), 114 | resultSet.getString("summary"), 115 | resultSet.getString("content"), 116 | resultSet.getInt("id"), 117 | resultSet.getDate("createdat"), 118 | resultSet.getBoolean("deleted") 119 | ); 120 | 121 | pstmt.close(); 122 | 123 | return (T) entity; 124 | } 125 | } catch(Exception e) { 126 | System.out.println(e.getMessage()); 127 | 128 | try { 129 | if(null != stmt) { 130 | stmt.close(); 131 | } 132 | if(null != conn) { 133 | conn.close(); 134 | } 135 | } catch (SQLException sqlException) { 136 | sqlException.printStackTrace(); 137 | } 138 | } 139 | 140 | return null; 141 | } 142 | 143 | @Override 144 | @SuppressWarnings("unchecked") //Tells the compiler to ignore unchecked type casts 145 | public ArrayList readAll() { 146 | // Type cast the generic T into an Article 147 | ArrayList
results = (ArrayList
) new ArrayList(); 148 | 149 | try { 150 | String query = "SELECT * FROM article;"; 151 | 152 | stmt.execute(query); 153 | ResultSet resultSet = stmt.getResultSet(); 154 | 155 | while(resultSet.next()) { 156 | Article entity = new Article( 157 | resultSet.getString("title"), 158 | resultSet.getString("summary"), 159 | resultSet.getString("content"), 160 | resultSet.getInt("id"), 161 | resultSet.getDate("createdat"), 162 | resultSet.getBoolean("deleted") 163 | ); 164 | 165 | results.add(entity); 166 | } 167 | } catch(Exception e) { 168 | System.out.println(e.getMessage()); 169 | 170 | try { 171 | if(null != stmt) { 172 | stmt.close(); 173 | } 174 | if(null != conn) { 175 | conn.close(); 176 | } 177 | } catch (SQLException sqlException) { 178 | sqlException.printStackTrace(); 179 | } 180 | } 181 | 182 | // The interface ArticleDbService relies upon the generic type T so we cast it back 183 | return (ArrayList) results; 184 | } 185 | 186 | @Override 187 | public Boolean update(int id, String title, String summary, String content) { 188 | try { 189 | String updateQuery = 190 | "UPDATE article SET title = ?, summary = ?, content = ?" + 191 | "WHERE id = ?;" 192 | ; 193 | 194 | PreparedStatement pstmt = conn.prepareStatement(updateQuery); 195 | 196 | pstmt.setString(1, title); 197 | pstmt.setString(2, summary); 198 | pstmt.setString(3, content); 199 | pstmt.setInt(4, id); 200 | 201 | pstmt.executeUpdate(); 202 | } catch(Exception e) { 203 | System.out.println(e.getMessage()); 204 | 205 | try { 206 | if(null != stmt) { 207 | stmt.close(); 208 | } 209 | if(null != conn) { 210 | conn.close(); 211 | } 212 | } catch (SQLException sqlException) { 213 | sqlException.printStackTrace(); 214 | } 215 | } 216 | 217 | return true; 218 | } 219 | 220 | @Override 221 | public Boolean delete(int id) { 222 | try { 223 | String deleteQuery = "DELETE FROM article WHERE id = ?"; 224 | 225 | PreparedStatement pstmt = conn.prepareStatement(deleteQuery); 226 | pstmt.setInt(1, id); 227 | 228 | pstmt.executeUpdate(); 229 | } catch (Exception e) { 230 | System.out.println(e.getMessage()); 231 | 232 | try { 233 | if(null != stmt) { 234 | stmt.close(); 235 | } 236 | if(null != conn) { 237 | conn.close(); 238 | } 239 | } catch (SQLException sqlException) { 240 | sqlException.printStackTrace(); 241 | } 242 | } 243 | 244 | return true; 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/main/java/ArticleServletDao.java: -------------------------------------------------------------------------------- 1 | import java.util.ArrayList; 2 | 3 | public class ArticleServletDao implements ArticleDbService { 4 | ArrayList storage; 5 | 6 | public ArticleServletDao() { 7 | storage = new ArrayList(); 8 | } 9 | 10 | @Override 11 | public Boolean create(T entity) { 12 | storage.add(entity); 13 | return null; 14 | } 15 | 16 | @Override 17 | public T readOne(int id) { 18 | return storage.get(id); 19 | } 20 | 21 | @Override 22 | public ArrayList readAll() { 23 | return storage; 24 | } 25 | 26 | @Override 27 | public Boolean update(int id, String title, String summary, String content) { 28 | T entity = storage.get(id); 29 | 30 | entity.setSummary(summary); 31 | entity.setTitle(title); 32 | entity.setContent(content); 33 | 34 | return true; 35 | } 36 | 37 | @Override 38 | public Boolean delete(int id) { 39 | storage.get(id).delete(); 40 | return true; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/HelloSpark.java: -------------------------------------------------------------------------------- 1 | import static spark.Spark.*; 2 | 3 | import spark.ModelAndView; 4 | import spark.Request; 5 | import spark.Response; 6 | import spark.Route; 7 | import spark.template.freemarker.FreeMarkerRoute; 8 | 9 | import java.util.*; 10 | 11 | public class HelloSpark { 12 | public static ArticleDbService
articleDbService = new ArticleServletDao
(); 13 | 14 | public static void main(String[] args) { 15 | get(new FreeMarkerRoute("/") { 16 | @Override 17 | public ModelAndView handle(Request request, Response response) { 18 | Map viewObjects = new HashMap(); 19 | ArrayList
articles = articleDbService.readAll(); 20 | 21 | if (articles.isEmpty()) { 22 | viewObjects.put("hasNoArticles", "Welcome, please click \"Write Article\" to begin."); 23 | } else { 24 | Deque
showArticles = new ArrayDeque
(); 25 | 26 | for (Article article : articles) { 27 | if (article.readable()) { 28 | showArticles.addFirst(article); 29 | } 30 | } 31 | 32 | viewObjects.put("articles", showArticles); 33 | } 34 | 35 | viewObjects.put("templateName", "articleList.ftl"); 36 | 37 | return modelAndView(viewObjects, "layout.ftl"); 38 | } 39 | }); 40 | 41 | get(new FreeMarkerRoute("/article/create") { 42 | @Override 43 | public Object handle(Request request, Response response) { 44 | Map viewObjects = new HashMap(); 45 | 46 | viewObjects.put("templateName", "articleForm.ftl"); 47 | 48 | return modelAndView(viewObjects, "layout.ftl"); 49 | } 50 | }); 51 | 52 | post(new Route("/article/create") { 53 | @Override 54 | public Object handle(Request request, Response response) { 55 | String title = request.queryParams("article-title"); 56 | String summary = request.queryParams("article-summary"); 57 | String content = request.queryParams("article-content"); 58 | 59 | Article article = new Article(title, summary, content, articleDbService.readAll().size()); 60 | 61 | articleDbService.create(article); 62 | 63 | response.status(201); 64 | response.redirect("/"); 65 | return ""; 66 | } 67 | }); 68 | 69 | get(new FreeMarkerRoute("/article/read/:id") { 70 | @Override 71 | public Object handle(Request request, Response response) { 72 | Integer id = Integer.parseInt(request.params(":id")); 73 | Map viewObjects = new HashMap(); 74 | 75 | viewObjects.put("templateName", "articleRead.ftl"); 76 | 77 | viewObjects.put("article", articleDbService.readOne(id)); 78 | 79 | return modelAndView(viewObjects, "layout.ftl"); 80 | } 81 | }); 82 | 83 | get(new FreeMarkerRoute("/article/update/:id") { 84 | @Override 85 | public Object handle(Request request, Response response) { 86 | Integer id = Integer.parseInt(request.params(":id")); 87 | Map viewObjects = new HashMap(); 88 | 89 | viewObjects.put("templateName", "articleForm.ftl"); 90 | 91 | viewObjects.put("article", articleDbService.readOne(id)); 92 | 93 | return modelAndView(viewObjects, "layout.ftl"); 94 | } 95 | }); 96 | 97 | post(new Route("/article/update/:id") { 98 | @Override 99 | public Object handle(Request request, Response response) { 100 | Integer id = Integer.parseInt(request.queryParams("article-id")); 101 | String title = request.queryParams("article-title"); 102 | String summary = request.queryParams("article-summary"); 103 | String content = request.queryParams("article-content"); 104 | 105 | articleDbService.update(id, title, summary, content); 106 | 107 | response.status(200); 108 | response.redirect("/"); 109 | return ""; 110 | } 111 | }); 112 | 113 | get(new Route("/article/delete/:id") { 114 | @Override 115 | public Object handle(Request request, Response response) { 116 | Integer id = Integer.parseInt(request.params(":id")); 117 | 118 | articleDbService.delete(id); 119 | 120 | response.status(200); 121 | response.redirect("/"); 122 | return ""; 123 | } 124 | }); 125 | } 126 | } -------------------------------------------------------------------------------- /src/main/resources/spark/template/freemarker/articleForm.ftl: -------------------------------------------------------------------------------- 1 |
2 |
action="/article/update/:id"<#else>action="/article/create"> 3 |
4 | 5 |
6 | value="${article.getTitle()}" /> 7 |
8 |
9 |
10 | 11 |
12 | value="${article.getSummary()}" /> 13 |
14 |
15 | <#if article??> 16 | 17 | 18 |
19 | 20 | 21 | 22 | value='Update'<#else>value='Publish' class="btn btn-primary" form='article-create-form' /> 23 |
-------------------------------------------------------------------------------- /src/main/resources/spark/template/freemarker/articleList.ftl: -------------------------------------------------------------------------------- 1 | <#if hasNoArticles??> 2 |
3 |

${hasNoArticles}

4 |
5 | <#else> 6 |
7 | <#list articles as article> 8 |

${article.getTitle()}

9 |

${article.getCreatedAt()}

10 |

${article.getSummaryLink()}

11 |

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

12 | 13 |
14 | -------------------------------------------------------------------------------- /src/main/resources/spark/template/freemarker/articleRead.ftl: -------------------------------------------------------------------------------- 1 |
2 |

${article.getTitle()}

3 |

${article.getCreatedAt()}

4 |

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

5 |
${article.getContent()}
6 |
-------------------------------------------------------------------------------- /src/main/resources/spark/template/freemarker/layout.ftl: -------------------------------------------------------------------------------- 1 | 2 | 3 | Spark Blog 4 | 5 | 6 | 7 | 8 | 9 | 10 | 28 | 29 |
30 | <#include "${templateName}"> 31 |
32 | 33 | 34 | 35 | 36 | 37 | --------------------------------------------------------------------------------