001/* 002 * (C) Copyright 2014-2018 Nuxeo (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * <a href="mailto:grenard@nuxeo.com">Guillaume Renard</a> 018 * <a href="mailto:ncunha@nuxeo.com">Nuno Cunha</a> 019 * 020 */ 021 022package org.nuxeo.ecm.platform.routing.core.io; 023 024import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; 025import static org.nuxeo.ecm.core.io.registry.reflect.Instantiations.SINGLETON; 026import static org.nuxeo.ecm.core.io.registry.reflect.Priorities.REFERENCE; 027 028import java.io.Closeable; 029import java.io.IOException; 030import java.io.OutputStream; 031import java.util.List; 032 033import javax.inject.Inject; 034 035import org.apache.commons.lang3.StringUtils; 036import org.nuxeo.ecm.core.api.CoreSession; 037import org.nuxeo.ecm.core.api.IdRef; 038import org.nuxeo.ecm.core.api.model.Property; 039import org.nuxeo.ecm.core.io.marshallers.json.ExtensibleEntityJsonWriter; 040import org.nuxeo.ecm.core.io.marshallers.json.OutputStreamWithJsonWriter; 041import org.nuxeo.ecm.core.io.marshallers.json.document.DocumentModelJsonWriter; 042import org.nuxeo.ecm.core.io.registry.MarshallerRegistry; 043import org.nuxeo.ecm.core.io.registry.Writer; 044import org.nuxeo.ecm.core.io.registry.context.RenderingContext; 045import org.nuxeo.ecm.core.io.registry.context.RenderingContext.SessionWrapper; 046import org.nuxeo.ecm.core.io.registry.reflect.Setup; 047import org.nuxeo.ecm.core.schema.SchemaManager; 048import org.nuxeo.ecm.core.schema.types.CompositeType; 049import org.nuxeo.ecm.core.schema.types.Field; 050import org.nuxeo.ecm.core.schema.types.Schema; 051import org.nuxeo.ecm.core.schema.utils.DateParser; 052import org.nuxeo.ecm.platform.actions.ActionContext; 053import org.nuxeo.ecm.platform.actions.ELActionContext; 054import org.nuxeo.ecm.platform.actions.ejb.ActionManager; 055import org.nuxeo.ecm.platform.routing.api.DocumentRoute; 056import org.nuxeo.ecm.platform.routing.api.DocumentRoutingConstants; 057import org.nuxeo.ecm.platform.routing.core.impl.GraphNode; 058import org.nuxeo.ecm.platform.routing.core.impl.GraphNode.Button; 059import org.nuxeo.ecm.platform.routing.core.impl.GraphRoute; 060import org.nuxeo.ecm.platform.task.Task; 061import org.nuxeo.ecm.platform.task.TaskComment; 062import org.nuxeo.ecm.platform.usermanager.UserManager; 063import org.nuxeo.runtime.api.Framework; 064 065import com.fasterxml.jackson.core.JsonGenerator; 066 067/** 068 * @since 7.2 069 */ 070@Setup(mode = SINGLETON, priority = REFERENCE) 071public class TaskWriter extends ExtensibleEntityJsonWriter<Task> { 072 073 public static final String FETCH_ACTORS = "actors"; 074 075 public static final String TARGET_DOCUMENT_IDS = "targetDocumentIds"; 076 077 public static final String FETCH_TARGET_DOCUMENT = TARGET_DOCUMENT_IDS; 078 079 public static final String FETCH_WORKFLOW_INITATIOR = "workflowInitiator"; 080 081 protected static final String USER_PREFIX = "user"; 082 083 protected static final String GROUP_PREFIX = "group"; 084 085 protected static final String SEPARATOR = ":"; 086 087 @Inject 088 protected SchemaManager schemaManager; 089 090 @Inject 091 protected UserManager userManager; 092 093 public TaskWriter() { 094 super(ENTITY_TYPE, Task.class); 095 } 096 097 public static final String ENTITY_TYPE = "task"; 098 099 @Override 100 public void writeEntityBody(Task item, JsonGenerator jg) throws IOException { 101 GraphRoute workflowInstance = null; 102 GraphNode node = null; 103 String workflowInstanceId = item.getProcessId(); 104 final String nodeId = item.getVariable(DocumentRoutingConstants.TASK_NODE_ID_KEY); 105 try (SessionWrapper wrapper = ctx.getSession(item.getDocument())) { 106 if (StringUtils.isNotBlank(workflowInstanceId)) { 107 NodeAccessRunner nodeAccessRunner = new NodeAccessRunner(wrapper.getSession(), workflowInstanceId, 108 nodeId); 109 nodeAccessRunner.runUnrestricted(); 110 workflowInstance = nodeAccessRunner.getWorkflowInstance(); 111 node = nodeAccessRunner.getNode(); 112 } 113 114 jg.writeStringField("id", item.getDocument().getId()); 115 jg.writeStringField("name", item.getName()); 116 jg.writeStringField("workflowInstanceId", workflowInstanceId); 117 if (workflowInstance != null) { 118 jg.writeStringField("workflowModelName", workflowInstance.getModelName()); 119 writeWorkflowInitiator(jg, workflowInstance.getInitiator()); 120 jg.writeStringField("workflowTitle", workflowInstance.getTitle()); 121 jg.writeStringField("workflowLifeCycleState", 122 workflowInstance.getDocument().getCurrentLifeCycleState()); 123 jg.writeStringField("graphResource", DocumentRouteWriter.getGraphResourceURL( 124 workflowInstance.getDocumentRoute(wrapper.getSession()), ctx)); 125 126 } 127 jg.writeStringField("state", item.getDocument().getCurrentLifeCycleState()); 128 jg.writeStringField("directive", item.getDirective()); 129 jg.writeStringField("created", DateParser.formatW3CDateTime(item.getCreated())); 130 jg.writeStringField("dueDate", DateParser.formatW3CDateTime(item.getDueDate())); 131 jg.writeStringField("nodeName", item.getVariable(DocumentRoutingConstants.TASK_NODE_ID_KEY)); 132 133 jg.writeArrayFieldStart(TARGET_DOCUMENT_IDS); 134 final boolean isFetchTargetDocumentIds = ctx.getFetched(ENTITY_TYPE).contains(FETCH_TARGET_DOCUMENT); 135 for (String docId : item.getTargetDocumentsIds()) { 136 IdRef idRef = new IdRef(docId); 137 if (wrapper.getSession().exists(idRef)) { 138 if (isFetchTargetDocumentIds) { 139 writeEntity(wrapper.getSession().getDocument(idRef), jg); 140 } else { 141 jg.writeStartObject(); 142 jg.writeStringField("id", docId); 143 jg.writeEndObject(); 144 } 145 } 146 } 147 jg.writeEndArray(); 148 149 final boolean isFetchActors = ctx.getFetched(ENTITY_TYPE).contains(FETCH_ACTORS); 150 jg.writeArrayFieldStart("actors"); 151 writeActors(item.getActors(), isFetchActors, jg); 152 jg.writeEndArray(); 153 154 jg.writeArrayFieldStart("delegatedActors"); 155 writeActors(item.getDelegatedActors(), isFetchActors, jg); 156 jg.writeEndArray(); 157 158 jg.writeArrayFieldStart("comments"); 159 for (TaskComment comment : item.getComments()) { 160 jg.writeStartObject(); 161 jg.writeStringField("author", comment.getAuthor()); 162 jg.writeStringField("text", comment.getText()); 163 jg.writeStringField("date", DateParser.formatW3CDateTime(comment.getCreationDate().getTime())); 164 jg.writeEndObject(); 165 } 166 jg.writeEndArray(); 167 168 jg.writeFieldName("variables"); 169 jg.writeStartObject(); 170 // add nodeVariables 171 if (node != null) { 172 writeTaskVariables(node, jg, registry, ctx, schemaManager); 173 } 174 // add workflow variables 175 if (workflowInstance != null) { 176 writeWorkflowVariables(workflowInstance, node, jg, registry, ctx, schemaManager); 177 } 178 jg.writeEndObject(); 179 180 if (node != null) { 181 jg.writeFieldName("taskInfo"); 182 jg.writeStartObject(); 183 jg.writeBooleanField("allowTaskReassignment", node.allowTaskReassignment()); 184 185 final ActionManager actionManager = Framework.getService(ActionManager.class); 186 jg.writeArrayFieldStart("taskActions"); 187 for (Button button : node.getTaskButtons()) { 188 if (StringUtils.isBlank(button.getFilter()) || actionManager.checkFilter(button.getFilter(), 189 createActionContext(wrapper.getSession()))) { 190 jg.writeStartObject(); 191 jg.writeStringField("name", button.getName()); 192 jg.writeStringField("url", ctx.getBaseUrl() + "api/v1/task/" + item.getDocument().getId() + "/" 193 + button.getName()); 194 jg.writeStringField("label", button.getLabel()); 195 jg.writeBooleanField("validate", button.getValidate()); 196 jg.writeEndObject(); 197 } 198 } 199 jg.writeEndArray(); 200 201 jg.writeFieldName("layoutResource"); 202 jg.writeStartObject(); 203 jg.writeStringField("name", node.getTaskLayout()); 204 jg.writeStringField("url", 205 ctx.getBaseUrl() + "site/layout-manager/layouts/?layoutName=" + node.getTaskLayout()); 206 jg.writeEndObject(); 207 208 jg.writeArrayFieldStart("schemas"); 209 for (String schema : node.getDocument().getSchemas()) { 210 // TODO only keep functional schema once adaptation done 211 jg.writeStartObject(); 212 jg.writeStringField("name", schema); 213 jg.writeStringField("url", ctx.getBaseUrl() + "api/v1/config/schemas/" + schema); 214 jg.writeEndObject(); 215 } 216 jg.writeEndArray(); 217 218 jg.writeEndObject(); 219 } 220 } 221 } 222 223 protected void writeActors(List<String> actors, boolean isFetchActors, JsonGenerator jg) throws IOException { 224 for (String actorId : actors) { 225 if (isFetchActors) { 226 Object actor = fetchActor(actorId); 227 if (actor != null) { 228 writeEntity(actor, jg); 229 continue; 230 } 231 } 232 jg.writeStartObject(); 233 jg.writeStringField("id", actorId); 234 jg.writeEndObject(); 235 } 236 } 237 238 protected Object fetchActor(String actorId) { 239 String unprefixedActorId = getUnprefixedActorId(actorId); 240 Object actor = userManager.getPrincipal(unprefixedActorId); 241 if (actor == null) { 242 actor = userManager.getGroup(unprefixedActorId); 243 } 244 return actor; 245 } 246 247 protected String getUnprefixedActorId(String actorId) { 248 if (actorId.contains(USER_PREFIX)) { 249 actorId = actorId.substring(USER_PREFIX.length() + SEPARATOR.length()); 250 } else if (actorId.contains(GROUP_PREFIX)) { 251 actorId = actorId.substring(GROUP_PREFIX.length() + SEPARATOR.length()); 252 } 253 return actorId; 254 } 255 256 protected void writeWorkflowInitiator(JsonGenerator jg, String workflowInitiator) throws IOException { 257 if (ctx.getFetched(ENTITY_TYPE).contains(FETCH_WORKFLOW_INITATIOR)) { 258 Object principal = fetchActor(workflowInitiator); 259 if (principal != null) { 260 writeEntityField("workflowInitiator", principal, jg); 261 return; 262 } 263 } 264 jg.writeStringField("workflowInitiator", workflowInitiator); 265 } 266 267 protected static ActionContext createActionContext(CoreSession session) { 268 ActionContext actionContext = new ELActionContext(); 269 actionContext.setDocumentManager(session); 270 actionContext.setCurrentPrincipal(session.getPrincipal()); 271 return actionContext; 272 } 273 274 /** 275 * @since 8.3 276 */ 277 public static void writeTaskVariables(GraphNode node, JsonGenerator jg, MarshallerRegistry registry, 278 RenderingContext ctx, SchemaManager schemaManager) throws IOException { 279 if (node == null || node.getDocument() == null) { 280 return; 281 } 282 String facet = (String) node.getDocument().getPropertyValue(GraphNode.PROP_VARIABLES_FACET); 283 if (StringUtils.isNotBlank(facet)) { 284 285 CompositeType type = schemaManager.getFacet(facet); 286 if (type != null) { 287 boolean hasFacet = node.getDocument().hasFacet(facet); 288 289 Writer<Property> propertyWriter = registry.getWriter(ctx, Property.class, APPLICATION_JSON_TYPE); 290 // provides the current route to the property marshaller 291 try (Closeable resource = ctx.wrap() 292 .with(DocumentModelJsonWriter.ENTITY_TYPE, node.getDocument()) 293 .open()) { 294 for (Field f : type.getFields()) { 295 String name = f.getName().getLocalName(); 296 Property property = hasFacet ? node.getDocument().getProperty(name) : null; 297 OutputStream out = new OutputStreamWithJsonWriter(jg); 298 jg.writeFieldName(name); 299 propertyWriter.write(property, Property.class, Property.class, APPLICATION_JSON_TYPE, out); 300 } 301 } 302 } 303 } 304 } 305 306 /** 307 * @since 8.3 308 */ 309 public static void writeWorkflowVariables(DocumentRoute route, GraphNode node, JsonGenerator jg, 310 MarshallerRegistry registry, RenderingContext ctx, SchemaManager schemaManager) throws IOException { 311 String facet = (String) route.getDocument().getPropertyValue(GraphRoute.PROP_VARIABLES_FACET); 312 if (StringUtils.isNotBlank(facet)) { 313 314 CompositeType type = schemaManager.getFacet(facet); 315 if (type != null) { 316 317 final String transientSchemaName = DocumentRoutingConstants.GLOBAL_VAR_SCHEMA_PREFIX + node.getId(); 318 final Schema transientSchema = schemaManager.getSchema(transientSchemaName); 319 if (transientSchema == null) { 320 return; 321 } 322 323 boolean hasFacet = route.getDocument().hasFacet(facet); 324 325 Writer<Property> propertyWriter = registry.getWriter(ctx, Property.class, APPLICATION_JSON_TYPE); 326 // provides the current route to the property marshaller 327 try (Closeable resource = ctx.wrap() 328 .with(DocumentModelJsonWriter.ENTITY_TYPE, route.getDocument()) 329 .open()) { 330 for (Field f : type.getFields()) { 331 String name = f.getName().getLocalName(); 332 if (transientSchema.hasField(name)) { 333 Property property = hasFacet ? route.getDocument().getProperty(name) : null; 334 OutputStream out = new OutputStreamWithJsonWriter(jg); 335 jg.writeFieldName(name); 336 propertyWriter.write(property, Property.class, Property.class, APPLICATION_JSON_TYPE, out); 337 } 338 } 339 } 340 } 341 } 342 } 343}