├── .gitignore ├── README.md ├── pom.xml └── src └── main ├── java └── de │ └── chkal │ └── todo │ ├── TodoApplication.java │ ├── ext │ └── LocalDateParamConverterProvider.java │ ├── service │ ├── TodoItem.java │ └── TodoService.java │ └── web │ ├── CreateItemForm.java │ ├── Messages.java │ └── TodoListController.java ├── resources └── META-INF │ └── validation.xml └── webapp ├── WEB-INF ├── beans.xml ├── views │ └── items.jsp └── web.xml └── index.jsp /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .project 3 | .settings 4 | .idea 5 | target 6 | *~ 7 | *.iml 8 | *.swp 9 | .DS_Store 10 | nb-configuration.xml 11 | nbactions.xml 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TODO MVC 2 | 3 | A simple TODO list application demonstrating how to build applications 4 | using the [MVC 1.0](https://jcp.org/en/jsr/detail?id=371) specification. 5 | 6 | ## Introduction 7 | 8 | MVC 1.0 is still at a very early stage. The final release is scheduled for Q3 2016 and will be part 9 | of Java EE 8. 10 | 11 | To be able to use the latest versions of all technologies which are required for MVC (like 12 | JAX-RS, CDI and Bean Validation), this application has been created to be deployed to a 13 | Servlet container like [Apache Tomcat](http://tomcat.apache.org/) or [Jetty](http://eclipse.org/jetty/). 14 | This allows to include all the requirements in the WAR file instead of having to rely on the 15 | container to provide them. 16 | 17 | ## Getting started 18 | 19 | The application is pretty minimal at the moment. I added comments to the relevant 20 | sections of the code so you don't have to read the specification for understanding 21 | what is going on. ;) 22 | 23 | To build the WAR file, run this command: 24 | 25 | $ mvn clean package 26 | 27 | The resulting WAR is named `target/todo-mvc.war`. You can now deploy this WAR file to 28 | [Apache Tomcat](http://tomcat.apache.org/) or [Jetty](http://eclipse.org/jetty/). 29 | If you are using Tomcat, you should use the latest release of the 8.0.x product line. 30 | 31 | After starting up, open the following URL in your browser: 32 | 33 | http://localhost:8080/todo-mvc/ 34 | 35 | 36 | ## Feedback welcome 37 | 38 | The MVC expert group is looking for feedback. Any feedback is valuable and welcome. 39 | So if there is anything that you would like us to know, get in touch with us. 40 | 41 | * Post on the public [user mailing list](https://java.net/projects/mvc-spec/lists), or 42 | * Create an [issue](https://java.net/jira/browse/MVC_SPEC) in the issue tracker. 43 | 44 | And remember: Now is the time to give feedback. If you don't, you cannot complain about 45 | it later. ;) 46 | 47 | 48 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | de.chkal.todo 7 | todo-mvc 8 | 1.0-SNAPSHOT 9 | war 10 | 11 | todo-mvc 12 | 13 | 14 | 1.8 15 | 1.8 16 | 2.34 17 | 18 | 19 | 20 | todo-mvc 21 | 22 | 23 | 24 | 25 | 26 | 27 | jakarta.servlet.jsp 28 | jakarta.servlet.jsp-api 29 | 2.3.6 30 | provided 31 | 32 | 33 | 34 | 35 | org.glassfish.jersey.containers 36 | jersey-container-servlet 37 | ${jersey.version} 38 | 39 | 40 | org.glassfish.jersey.inject 41 | jersey-hk2 42 | ${jersey.version} 43 | 44 | 45 | org.glassfish.jersey.ext.cdi 46 | jersey-cdi1x 47 | ${jersey.version} 48 | 49 | 50 | org.glassfish.jersey.ext 51 | jersey-bean-validation 52 | ${jersey.version} 53 | 54 | 55 | jakarta.enterprise 56 | jakarta.enterprise.cdi-api 57 | 2.0.2 58 | 59 | 60 | org.jboss.weld.servlet 61 | weld-servlet-core 62 | 3.1.7.SP1 63 | 64 | 65 | org.hibernate 66 | hibernate-validator-cdi 67 | 6.2.0.Final 68 | 69 | 70 | 71 | 72 | jakarta.mvc 73 | jakarta.mvc-api 74 | 1.1.0 75 | 76 | 77 | org.eclipse.krazo 78 | krazo-jersey 79 | 1.1.0 80 | 81 | 82 | 83 | 84 | org.glassfish.web 85 | jakarta.servlet.jsp.jstl 86 | 1.2.6 87 | 88 | 89 | 90 | 91 | org.webjars.bower 92 | jquery 93 | 1.11.0 94 | 95 | 96 | org.webjars.bower 97 | bootstrap 98 | 3.3.4 99 | 100 | 101 | org.webjars 102 | bootstrap-datepicker 103 | 1.4.0 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /src/main/java/de/chkal/todo/TodoApplication.java: -------------------------------------------------------------------------------- 1 | package de.chkal.todo; 2 | 3 | import javax.mvc.security.Csrf; 4 | import javax.ws.rs.ApplicationPath; 5 | import javax.ws.rs.core.Application; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | /** 10 | * Application class required by JAX-RS. If you don't want to have any 11 | * prefix in the URL, you can set the application path to "/". 12 | */ 13 | @ApplicationPath("/r") 14 | public class TodoApplication extends Application { 15 | 16 | @Override 17 | public Map getProperties() { 18 | Map properties = new HashMap<>(); 19 | 20 | /* 21 | * Enables CSRF protection. If enabled, you will have to add an 22 | * hidden input to your forms which wil contains the required token. 23 | * See items.jsp for an example. 24 | */ 25 | properties.put(Csrf.CSRF_PROTECTION, Csrf.CsrfOptions.IMPLICIT); 26 | 27 | return properties; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/de/chkal/todo/ext/LocalDateParamConverterProvider.java: -------------------------------------------------------------------------------- 1 | package de.chkal.todo.ext; 2 | 3 | import javax.ws.rs.ext.ParamConverter; 4 | import javax.ws.rs.ext.ParamConverterProvider; 5 | import javax.ws.rs.ext.Provider; 6 | import java.lang.annotation.Annotation; 7 | import java.lang.reflect.Type; 8 | import java.time.LocalDate; 9 | import java.time.format.DateTimeFormatter; 10 | 11 | @Provider 12 | public class LocalDateParamConverterProvider implements ParamConverterProvider { 13 | 14 | private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_DATE; 15 | 16 | @Override 17 | public ParamConverter getConverter(Class rawType, Type genericType, Annotation[] annotations) { 18 | 19 | if (LocalDate.class.equals(rawType)) { 20 | 21 | return new ParamConverter() { 22 | 23 | @Override 24 | public T fromString(String value) { 25 | if (value != null && value.trim().length() > 0) { 26 | return (T) LocalDate.parse(value, FORMATTER); 27 | } 28 | return null; 29 | } 30 | 31 | @Override 32 | public String toString(T value) { 33 | return ((LocalDate) value).format(FORMATTER); 34 | } 35 | 36 | }; 37 | 38 | } 39 | 40 | return null; 41 | 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/de/chkal/todo/service/TodoItem.java: -------------------------------------------------------------------------------- 1 | package de.chkal.todo.service; 2 | 3 | import java.time.LocalDate; 4 | 5 | /** 6 | * The model object representing a single item. 7 | * Could be an JPA entity in real world applications. 8 | */ 9 | public class TodoItem { 10 | 11 | public enum Priority { 12 | LOW, 13 | MEDIUM, 14 | HIGH 15 | } 16 | 17 | private long id; 18 | 19 | private String title; 20 | 21 | private Priority priority; 22 | 23 | private LocalDate dueDate; 24 | 25 | public TodoItem() { 26 | // default constructor 27 | } 28 | 29 | public TodoItem(long id, String title, Priority priority, LocalDate dueDate) { 30 | this.id = id; 31 | this.title = title; 32 | this.priority = priority; 33 | this.dueDate = dueDate; 34 | } 35 | 36 | public String getTitle() { 37 | return title; 38 | } 39 | 40 | public void setTitle(String title) { 41 | this.title = title; 42 | } 43 | 44 | public long getId() { 45 | return id; 46 | } 47 | 48 | public void setId(long id) { 49 | this.id = id; 50 | } 51 | 52 | public Priority getPriority() { 53 | return priority; 54 | } 55 | 56 | public void setPriority(Priority priority) { 57 | this.priority = priority; 58 | } 59 | 60 | public LocalDate getDueDate() { 61 | return dueDate; 62 | } 63 | 64 | public void setDueDate(LocalDate dueDate) { 65 | this.dueDate = dueDate; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/de/chkal/todo/service/TodoService.java: -------------------------------------------------------------------------------- 1 | package de.chkal.todo.service; 2 | 3 | import javax.annotation.PostConstruct; 4 | import javax.enterprise.context.ApplicationScoped; 5 | import java.time.LocalDate; 6 | import java.util.ArrayList; 7 | import java.util.Collections; 8 | import java.util.Iterator; 9 | import java.util.List; 10 | 11 | @ApplicationScoped 12 | public class TodoService { 13 | 14 | private final List items = new ArrayList<>(); 15 | 16 | private long sequence = 1; 17 | 18 | @PostConstruct 19 | public void init() { 20 | createItem("Create MVC sample app", TodoItem.Priority.HIGH, LocalDate.now().plusDays(5)); 21 | createItem("Write a blog post about it", TodoItem.Priority.MEDIUM, null); 22 | } 23 | 24 | public List getItems() { 25 | return Collections.unmodifiableList(items); 26 | } 27 | 28 | public TodoItem createItem(String title, TodoItem.Priority priority, LocalDate dueDate) { 29 | TodoItem item = new TodoItem(sequence++, title, priority, dueDate); 30 | items.add(item); 31 | return item; 32 | } 33 | 34 | public void deleteItem(long id) { 35 | Iterator it = items.iterator(); 36 | while (it.hasNext()) { 37 | TodoItem item = it.next(); 38 | if (item.getId() == id) { 39 | it.remove(); 40 | } 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/de/chkal/todo/web/CreateItemForm.java: -------------------------------------------------------------------------------- 1 | package de.chkal.todo.web; 2 | 3 | import de.chkal.todo.service.TodoItem; 4 | 5 | import javax.validation.constraints.NotNull; 6 | import javax.validation.constraints.Size; 7 | import javax.ws.rs.FormParam; 8 | import java.time.LocalDate; 9 | 10 | /** 11 | * This class represents the form which is submitted when the user creates a new item. 12 | * The classes uses {@link FormParam} to map the form data to fields. The fields are 13 | * annotated with Bean Validation annotations to describe constraints. 14 | */ 15 | public class CreateItemForm { 16 | 17 | @NotNull 18 | @Size(min = 3, message = "The title must be at least 3 characters") 19 | @FormParam("title") 20 | private String title; 21 | 22 | @NotNull(message = "Please select the priority of the task") 23 | @FormParam("priority") 24 | private TodoItem.Priority priority; 25 | 26 | @FormParam("duedate") 27 | private LocalDate dueDate; 28 | 29 | public String getTitle() { 30 | return title; 31 | } 32 | 33 | public void setTitle(String title) { 34 | this.title = title; 35 | } 36 | 37 | public TodoItem.Priority getPriority() { 38 | return priority; 39 | } 40 | 41 | public void setPriority(TodoItem.Priority priority) { 42 | this.priority = priority; 43 | } 44 | 45 | public LocalDate getDueDate() { 46 | return dueDate; 47 | } 48 | 49 | public void setDueDate(LocalDate dueDate) { 50 | this.dueDate = dueDate; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/de/chkal/todo/web/Messages.java: -------------------------------------------------------------------------------- 1 | package de.chkal.todo.web; 2 | 3 | import javax.inject.Named; 4 | import javax.mvc.RedirectScoped; 5 | import java.io.Serializable; 6 | import java.util.ArrayList; 7 | import java.util.Collection; 8 | import java.util.Collections; 9 | import java.util.List; 10 | 11 | /** 12 | * This class encapsulates messages displayed to the users. There can be a 13 | * single info message and multiple error messages. Controllers can use this 14 | * class to queue messages for rendering. The class shows how named CDI beans 15 | * can be used as a model for the view. Please note that this class 16 | * uses the redirect scope to preserve messages across redirects. 17 | */ 18 | @Named 19 | @RedirectScoped 20 | public class Messages implements Serializable { 21 | 22 | private static final long serialVersionUID = 6012270416224546642L; 23 | 24 | private String info; 25 | 26 | private final List errors = new ArrayList<>(); 27 | 28 | public Messages addError(String error) { 29 | errors.add(error); 30 | return this; 31 | } 32 | 33 | public Messages addErrors(Collection errors) { 34 | this.errors.addAll(errors); 35 | return this; 36 | } 37 | 38 | public List getErrors() { 39 | return Collections.unmodifiableList(errors); 40 | } 41 | 42 | public String getInfo() { 43 | return info; 44 | } 45 | 46 | public void setInfo(String info) { 47 | this.info = info; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/de/chkal/todo/web/TodoListController.java: -------------------------------------------------------------------------------- 1 | package de.chkal.todo.web; 2 | 3 | import de.chkal.todo.service.TodoItem; 4 | import de.chkal.todo.service.TodoService; 5 | 6 | import javax.inject.Inject; 7 | import javax.mvc.Controller; 8 | import javax.mvc.Models; 9 | import javax.mvc.binding.BindingResult; 10 | import javax.validation.Valid; 11 | import javax.ws.rs.BeanParam; 12 | import javax.ws.rs.FormParam; 13 | import javax.ws.rs.GET; 14 | import javax.ws.rs.POST; 15 | import javax.ws.rs.Path; 16 | 17 | /** 18 | * A simple MVC controller. 19 | */ 20 | @Path("/items") 21 | @Controller 22 | public class TodoListController { 23 | 24 | @Inject 25 | private Models models; 26 | 27 | @Inject 28 | private BindingResult bindingResult; 29 | 30 | @Inject 31 | private Messages messages; 32 | 33 | @Inject 34 | private TodoService todoService; 35 | 36 | /** 37 | * Executed when the user navigates to the page. The method uses MVC's Models 38 | * class to populates the model with all data required by the view. 39 | */ 40 | @GET 41 | public String listItems() { 42 | models.put("items", todoService.getItems()); 43 | return "items.jsp"; 44 | } 45 | 46 | /** 47 | * Handles data submitted by the "create item" form. The form data is validated 48 | * using Bean Validation annotations. The method uses MVC's BindingResult class 49 | * to access the detected binding errors and constraint violations. If errors 50 | * were found, they are added to the custom Messages class which is used by 51 | * the view for rendering. 52 | */ 53 | @POST 54 | @Path("/create") 55 | public String createItem(@BeanParam @Valid CreateItemForm form) { 56 | 57 | if (bindingResult.isFailed()) { 58 | 59 | // store errors to display them in the view 60 | messages.addErrors(bindingResult.getAllMessages()); 61 | 62 | // The inputs should be populated with the previously submitted invalid values 63 | models.put("form", form); 64 | 65 | // reuse the listItems() controller method to prepare the model for rendering 66 | return listItems(); 67 | 68 | } 69 | 70 | TodoItem newItem = todoService.createItem( 71 | form.getTitle(), form.getPriority(), form.getDueDate()); 72 | 73 | /* 74 | * Redirect the user after that so that pressing F5 in the browser 75 | * doesn't trigger the form post again (POST-Redirect-GET pattern). 76 | */ 77 | messages.setInfo("Item created: " + newItem.getTitle()); 78 | return "redirect:/items"; 79 | 80 | } 81 | 82 | /** 83 | * Handles deletion of items. Works basically the same as the createItem() 84 | * method. The method doesn't use a separate from class, because there 85 | * is only a single hidden field representing the id of the item. 86 | */ 87 | @POST 88 | @Path("/delete") 89 | public String deleteItem(@FormParam("id") long id) { 90 | 91 | todoService.deleteItem(id); 92 | 93 | return "redirect:/items"; 94 | 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/validation.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 14 | 15 | 16 | NONE 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/beans.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/items.jsp: -------------------------------------------------------------------------------- 1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> 2 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | TODO MVC 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 |

TODO List

28 | 29 | 30 | 31 | 34 | 35 | 36 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 67 | 70 | 73 | 82 | 83 | 84 | 85 |
TitlePriorityDue DateActions
64 | 65 | ${mvc.encoders.html(item.title)} 66 | 68 | ${item.priority} 69 | 71 | ${item.dueDate} 72 | 74 |
75 | 76 | 77 | 80 |
81 |
86 | 87 |
89 | 90 |
91 | 93 |
94 | 95 |
96 | 102 |
103 | 104 |
105 | 107 | 117 |
118 | 119 | 120 | 121 | 122 | 123 | 124 |
125 | 126 |
127 | 128 | 129 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/webapp/index.jsp: -------------------------------------------------------------------------------- 1 | <%@ page import="javax.enterprise.inject.spi.CDI" %> 2 | <%@ page import="javax.mvc.MvcContext" %> 3 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> 4 | <% 5 | MvcContext mvc = CDI.current().select(MvcContext.class).get(); 6 | response.sendRedirect(mvc.getBasePath() + "/items"); 7 | %> --------------------------------------------------------------------------------