diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/RouteDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/RouteDao.java
new file mode 100644
index 00000000..f202b58f
--- /dev/null
+++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/RouteDao.java
@@ -0,0 +1,39 @@
+package com.sismics.docs.core.dao.jpa;
+
+import com.sismics.docs.core.constant.AuditLogType;
+import com.sismics.docs.core.model.jpa.Route;
+import com.sismics.docs.core.util.AuditLogUtil;
+import com.sismics.util.context.ThreadLocalContext;
+
+import javax.persistence.EntityManager;
+import java.util.Date;
+import java.util.UUID;
+
+/**
+ * Route DAO.
+ *
+ * @author bgamard
+ */
+public class RouteDao {
+ /**
+ * Creates a new route.
+ *
+ * @param route Route
+ * @param userId User ID
+ * @return New ID
+ */
+ public String create(Route route, String userId) {
+ // Create the UUID
+ route.setId(UUID.randomUUID().toString());
+
+ // Create the route
+ EntityManager em = ThreadLocalContext.get().getEntityManager();
+ route.setCreateDate(new Date());
+ em.persist(route);
+
+ // Create audit log
+ AuditLogUtil.create(route, AuditLogType.CREATE, userId);
+
+ return route.getId();
+ }
+}
diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/RouteStepDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/RouteStepDao.java
new file mode 100644
index 00000000..7b6bcc6c
--- /dev/null
+++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/RouteStepDao.java
@@ -0,0 +1,33 @@
+package com.sismics.docs.core.dao.jpa;
+
+import com.sismics.docs.core.model.jpa.RouteStep;
+import com.sismics.util.context.ThreadLocalContext;
+
+import javax.persistence.EntityManager;
+import java.util.Date;
+import java.util.UUID;
+
+/**
+ * Route step DAO.
+ *
+ * @author bgamard
+ */
+public class RouteStepDao {
+ /**
+ * Creates a new route step.
+ *
+ * @param routeStep Route step
+ * @return New ID
+ */
+ public String create(RouteStep routeStep) {
+ // Create the UUID
+ routeStep.setId(UUID.randomUUID().toString());
+
+ // Create the route step
+ EntityManager em = ThreadLocalContext.get().getEntityManager();
+ routeStep.setCreateDate(new Date());
+ em.persist(routeStep);
+
+ return routeStep.getId();
+ }
+}
diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Route.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Route.java
index b71641bb..e597a077 100644
--- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Route.java
+++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Route.java
@@ -15,7 +15,7 @@ import java.util.Date;
*/
@Entity
@Table(name = "T_ROUTE")
-public class Route {
+public class Route implements Loggable {
/**
* Route ID.
*/
@@ -29,6 +29,12 @@ public class Route {
@Column(name = "RTE_IDDOCUMENT_C", nullable = false, length = 36)
private String documentId;
+ /**
+ * Name.
+ */
+ @Column(name = "RTE_NAME_C", nullable = false, length = 50)
+ private String name;
+
/**
* Creation date.
*/
@@ -41,10 +47,61 @@ public class Route {
@Column(name = "RTE_DELETEDATE_D")
private Date deleteDate;
+ public String getId() {
+ return id;
+ }
+
+ public Route setId(String id) {
+ this.id = id;
+ return this;
+ }
+
+ public String getDocumentId() {
+ return documentId;
+ }
+
+ public Route setDocumentId(String documentId) {
+ this.documentId = documentId;
+ return this;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Route setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Date getCreateDate() {
+ return createDate;
+ }
+
+ public Route setCreateDate(Date createDate) {
+ this.createDate = createDate;
+ return this;
+ }
+
+ public Date getDeleteDate() {
+ return deleteDate;
+ }
+
+ public Route setDeleteDate(Date deleteDate) {
+ this.deleteDate = deleteDate;
+ return this;
+ }
+
+ @Override
+ public String toMessage() {
+ return documentId;
+ }
+
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("id", id)
+ .add("name", name)
.add("documentId", documentId)
.add("createDate", createDate)
.toString();
diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/RouteStep.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/RouteStep.java
index 83df4f94..c1a6b477 100644
--- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/RouteStep.java
+++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/RouteStep.java
@@ -84,6 +84,105 @@ public class RouteStep {
@Column(name = "RTP_DELETEDATE_D")
private Date deleteDate;
+ public String getId() {
+ return id;
+ }
+
+ public RouteStep setId(String id) {
+ this.id = id;
+ return this;
+ }
+
+ public String getRouteId() {
+ return routeId;
+ }
+
+ public RouteStep setRouteId(String routeId) {
+ this.routeId = routeId;
+ return this;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public RouteStep setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public RouteStepType getType() {
+ return type;
+ }
+
+ public RouteStep setType(RouteStepType type) {
+ this.type = type;
+ return this;
+ }
+
+ public RouteStepTransition getTransition() {
+ return transition;
+ }
+
+ public RouteStep setTransition(RouteStepTransition transition) {
+ this.transition = transition;
+ return this;
+ }
+
+ public String getComment() {
+ return comment;
+ }
+
+ public RouteStep setComment(String comment) {
+ this.comment = comment;
+ return this;
+ }
+
+ public String getTargetId() {
+ return targetId;
+ }
+
+ public RouteStep setTargetId(String targetId) {
+ this.targetId = targetId;
+ return this;
+ }
+
+ public Integer getOrder() {
+ return order;
+ }
+
+ public RouteStep setOrder(Integer order) {
+ this.order = order;
+ return this;
+ }
+
+ public Date getCreateDate() {
+ return createDate;
+ }
+
+ public RouteStep setCreateDate(Date createDate) {
+ this.createDate = createDate;
+ return this;
+ }
+
+ public Date getEndDate() {
+ return endDate;
+ }
+
+ public RouteStep setEndDate(Date endDate) {
+ this.endDate = endDate;
+ return this;
+ }
+
+ public Date getDeleteDate() {
+ return deleteDate;
+ }
+
+ public RouteStep setDeleteDate(Date deleteDate) {
+ this.deleteDate = deleteDate;
+ return this;
+ }
+
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
diff --git a/docs-core/src/main/resources/db/update/dbupdate-015-0.sql b/docs-core/src/main/resources/db/update/dbupdate-015-0.sql
index cba91ec2..11ea1fd4 100644
--- a/docs-core/src/main/resources/db/update/dbupdate-015-0.sql
+++ b/docs-core/src/main/resources/db/update/dbupdate-015-0.sql
@@ -1,6 +1,6 @@
create table T_ROUTE_MODEL ( RTM_ID_C varchar(36) not null, RTM_NAME_C varchar(50) not null, RTM_STEPS_C varchar(5000) not null, RTM_CREATEDATE_D datetime not null, RTM_DELETEDATE_D datetime, primary key (RTM_ID_C) );
-create cached table T_ROUTE ( RTE_ID_C varchar(36) not null, RTE_IDDOCUMENT_C varchar(36) not null, RTE_CREATEDATE_D datetime not null, RTE_DELETEDATE_D datetime, primary key (RTE_ID_C) );
-create cached table T_ROUTE_STEP ( RTP_ID_C varchar(36) not null, RTP_IDROUTE_C varchar(36) not null, RTP_NAME_C varchar(200) not null, RTP_TYPE_C varchar(50) not null, RTP_TRANSITION_C varchar(50), RTP_COMMENT_C varchar(500), RTP_IDTARGET_C varchar(36) not null, RTP_ORDER_N int not null, RTE_CREATEDATE_D datetime not null, RTP_ENDDATE_D datetime, RTP_DELETEDATE_D datetime, primary key (RTP_ID_C) );;
+create cached table T_ROUTE ( RTE_ID_C varchar(36) not null, RTE_IDDOCUMENT_C varchar(36) not null, RTE_NAME_C varchar(50) not null, RTE_CREATEDATE_D datetime not null, RTE_DELETEDATE_D datetime, primary key (RTE_ID_C) );
+create cached table T_ROUTE_STEP ( RTP_ID_C varchar(36) not null, RTP_IDROUTE_C varchar(36) not null, RTP_NAME_C varchar(200) not null, RTP_TYPE_C varchar(50) not null, RTP_TRANSITION_C varchar(50), RTP_COMMENT_C varchar(500), RTP_IDTARGET_C varchar(36) not null, RTP_ORDER_N int not null, RTP_CREATEDATE_D datetime not null, RTP_ENDDATE_D datetime, RTP_DELETEDATE_D datetime, primary key (RTP_ID_C) );;
alter table T_ROUTE add constraint FK_RTE_IDDOCUMENT_C foreign key (RTE_IDDOCUMENT_C) references T_DOCUMENT (DOC_ID_C) on delete restrict on update restrict;
alter table T_ROUTE_STEP add constraint FK_RTP_IDROUTE_C foreign key (RTP_IDROUTE_C) references T_ROUTE (RTE_ID_C) on delete restrict on update restrict;
diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteModelResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteModelResource.java
index cd5c4f4f..9db56f2e 100644
--- a/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteModelResource.java
+++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteModelResource.java
@@ -201,7 +201,8 @@ public class RouteModelResource extends BaseResource {
// Validate input
name = ValidationUtil.validateLength(name, "name", 1, 50, false);
- // TODO Validate steps data
+ steps = ValidationUtil.validateLength(steps, "steps", 1, 5000, false);
+ validateRouteModelSteps(steps);
// Get the route model
RouteModelDao routeModelDao = new RouteModelDao();
diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteResource.java
new file mode 100644
index 00000000..337dc8d8
--- /dev/null
+++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteResource.java
@@ -0,0 +1,118 @@
+package com.sismics.docs.rest.resource;
+
+import com.sismics.docs.core.constant.AclTargetType;
+import com.sismics.docs.core.constant.PermType;
+import com.sismics.docs.core.constant.RouteStepType;
+import com.sismics.docs.core.dao.jpa.*;
+import com.sismics.docs.core.model.jpa.*;
+import com.sismics.rest.exception.ClientException;
+import com.sismics.rest.exception.ForbiddenClientException;
+
+import javax.json.*;
+import javax.ws.rs.FormParam;
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.Response;
+import java.io.StringReader;
+
+/**
+ * Route REST resources.
+ *
+ * @author bgamard
+ */
+@Path("/route")
+public class RouteResource extends BaseResource {
+ /**
+ * Start a route on a document.
+ *
+ * @api {post} /route/start Start a route on a document
+ * @apiName PostRouteStart
+ * @apiRouteModel Route
+ * @apiParam {String} routeModelId Route model ID
+ * @apiParam {String} documentId Document ID
+ * @apiSuccess {String} status Status OK
+ * @apiError (client) InvalidRouteModel Invalid route model
+ * @apiError (client) ForbiddenError Access denied
+ * @apiError (client) NotFound Route model or document not found
+ * @apiPermission user
+ * @apiVersion 1.5.0
+ *
+ * @return Response
+ */
+ @POST
+ @Path("start")
+ public Response start(@FormParam("routeModelId") String routeModelId,
+ @FormParam("documentId") String documentId) {
+ if (!authenticate()) {
+ throw new ForbiddenClientException();
+ }
+
+ // Get the document
+ AclDao aclDao = new AclDao();
+ if (!aclDao.checkPermission(documentId, PermType.WRITE, getTargetIdList(null))) {
+ throw new NotFoundException();
+ }
+
+ // Get the route model
+ RouteModelDao routeModelDao = new RouteModelDao();
+ RouteModel routeModel = routeModelDao.getActiveById(routeModelId);
+ if (routeModel == null) {
+ throw new NotFoundException();
+ }
+
+ // Create the route
+ Route route = new Route()
+ .setDocumentId(documentId)
+ .setName(routeModel.getName());
+ RouteDao routeDao = new RouteDao();
+ routeDao.create(route, principal.getId());
+
+ // Create the steps
+ UserDao userDao = new UserDao();
+ GroupDao groupDao = new GroupDao();
+ RouteStepDao routeStepDao = new RouteStepDao();
+ try (JsonReader reader = Json.createReader(new StringReader(routeModel.getSteps()))) {
+ JsonArray stepsJson = reader.readArray();
+ int order = 0;
+ for (int i = 0; i < stepsJson.size(); i++) {
+ JsonObject step = stepsJson.getJsonObject(i);
+ JsonObject target = step.getJsonObject("target");
+ AclTargetType targetType = AclTargetType.valueOf(target.getString("type"));
+ String targetName = target.getString("name");
+
+ RouteStep routeStep = new RouteStep()
+ .setRouteId(route.getId())
+ .setName(step.getString("name"))
+ .setOrder(order++)
+ .setType(RouteStepType.valueOf(step.getString("type")));
+
+ switch (targetType) {
+ case USER:
+ User user = userDao.getActiveByUsername(targetName);
+ if (user != null) {
+ routeStep.setTargetId(user.getId());
+ }
+ break;
+ case GROUP:
+ Group group = groupDao.getActiveByName(targetName);
+ if (group != null) {
+ routeStep.setTargetId(group.getId());
+ }
+ break;
+ }
+
+ if (routeStep.getTargetId() == null) {
+ throw new ClientException("InvalidRouteModel", "A step has an invalid target");
+ }
+
+ routeStepDao.create(routeStep);
+ }
+ }
+
+ // Always return OK
+ JsonObjectBuilder response = Json.createObjectBuilder()
+ .add("status", "ok");
+ return Response.ok().entity(response.build()).build();
+ }
+}
diff --git a/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html b/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html
index 691b9d30..a4f28d2b 100644
--- a/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html
+++ b/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html
@@ -45,6 +45,9 @@
{{ log.message }}
+
+ {{ 'open' | translate }}
+
diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java
new file mode 100644
index 00000000..e628c62a
--- /dev/null
+++ b/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java
@@ -0,0 +1,56 @@
+package com.sismics.docs.rest;
+
+import com.sismics.util.filter.TokenBasedSecurityFilter;
+import org.junit.Assert;
+import org.junit.Test;
+
+import javax.json.JsonArray;
+import javax.json.JsonObject;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.Form;
+import java.util.Date;
+
+
+/**
+ * Test the route resource.
+ *
+ * @author bgamard
+ */
+public class TestRouteResource extends BaseJerseyTest {
+ /**
+ * Test the route resource.
+ */
+ @Test
+ public void testRouteResource() {
+ // Login admin
+ String adminToken = clientUtil.login("admin", "admin", false);
+
+ // Get all route models
+ JsonObject json = target().path("/routemodel")
+ .queryParam("sort_column", "2")
+ .queryParam("asc", "false")
+ .request()
+ .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
+ .get(JsonObject.class);
+ JsonArray routeModels = json.getJsonArray("routemodels");
+ Assert.assertEquals(1, routeModels.size());
+
+ // Create a document
+ long create1Date = new Date().getTime();
+ json = target().path("/document").request()
+ .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
+ .put(Entity.form(new Form()
+ .param("title", "My super title document 1")
+ .param("description", "My super description for document 1")
+ .param("language", "eng")
+ .param("create_date", Long.toString(create1Date))), JsonObject.class);
+ String document1Id = json.getString("id");
+
+ // Start the default route on document1
+ target().path("/route/start").request()
+ .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
+ .post(Entity.form(new Form()
+ .param("documentId", document1Id)
+ .param("routeModelId", routeModels.getJsonObject(0).getString("id"))), JsonObject.class);
+ }
+}
\ No newline at end of file