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 * 019 */ 020 021package org.nuxeo.ecm.platform.routing.core.io; 022 023import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; 024import static org.nuxeo.ecm.core.io.registry.reflect.Instantiations.SINGLETON; 025import static org.nuxeo.ecm.core.io.registry.reflect.Priorities.REFERENCE; 026 027import java.io.Closeable; 028import java.io.IOException; 029import java.io.OutputStream; 030 031import javax.inject.Inject; 032 033import org.apache.commons.lang3.StringUtils; 034import org.nuxeo.ecm.core.api.CoreSession; 035import org.nuxeo.ecm.core.api.IdRef; 036import org.nuxeo.ecm.core.api.NuxeoGroup; 037import org.nuxeo.ecm.core.api.NuxeoPrincipal; 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.JsonGenerationException; 066import com.fasterxml.jackson.core.JsonGenerator; 067 068/** 069 * @since 7.2 070 */ 071@Setup(mode = SINGLETON, priority = REFERENCE) 072public class TaskWriter extends ExtensibleEntityJsonWriter<Task> { 073 074 public static final String FETCH_ACTORS = "actors"; 075 076 public static final String TARGET_DOCUMENT_IDS = "targetDocumentIds"; 077 078 public static final String FETCH_TARGET_DOCUMENT = TARGET_DOCUMENT_IDS; 079 080 public static final String FETCH_WORKFLOW_INITATIOR = "workflowInitiator"; 081 082 protected static final String USER_PREFIX = "user"; 083 084 protected static final String GROUP_PREFIX = "group"; 085 086 protected static final String SEPARATOR = ":"; 087 088 @Inject 089 private SchemaManager schemaManager; 090 091 @Inject 092 UserManager userManager; 093 094 public TaskWriter() { 095 super(ENTITY_TYPE, Task.class); 096 } 097 098 public static final String ENTITY_TYPE = "task"; 099 100 @Override 101 public void writeEntityBody(Task item, JsonGenerator jg) throws IOException { 102 GraphRoute workflowInstance = null; 103 GraphNode node = null; 104 String workflowInstanceId = item.getProcessId(); 105 final String nodeId = item.getVariable(DocumentRoutingConstants.TASK_NODE_ID_KEY); 106 try (SessionWrapper wrapper = ctx.getSession(item.getDocument())) { 107 if (StringUtils.isNotBlank(workflowInstanceId)) { 108 NodeAccessRunner nodeAccessRunner = new NodeAccessRunner(wrapper.getSession(), workflowInstanceId, 109 nodeId); 110 nodeAccessRunner.runUnrestricted(); 111 workflowInstance = nodeAccessRunner.getWorkflowInstance(); 112 node = nodeAccessRunner.getNode(); 113 } 114 115 jg.writeStringField("id", item.getDocument().getId()); 116 jg.writeStringField("name", item.getName()); 117 jg.writeStringField("workflowInstanceId", workflowInstanceId); 118 if (workflowInstance != null) { 119 jg.writeStringField("workflowModelName", workflowInstance.getModelName()); 120 writeWorkflowInitiator(jg, workflowInstance.getInitiator()); 121 jg.writeStringField("workflowTitle", workflowInstance.getTitle()); 122 jg.writeStringField("workflowLifeCycleState", 123 workflowInstance.getDocument().getCurrentLifeCycleState()); 124 jg.writeStringField("graphResource", DocumentRouteWriter.getGraphResourceURL( 125 workflowInstance.getDocumentRoute(wrapper.getSession()), ctx)); 126 127 } 128 jg.writeStringField("state", item.getDocument().getCurrentLifeCycleState()); 129 jg.writeStringField("directive", item.getDirective()); 130 jg.writeStringField("created", DateParser.formatW3CDateTime(item.getCreated())); 131 jg.writeStringField("dueDate", DateParser.formatW3CDateTime(item.getDueDate())); 132 jg.writeStringField("nodeName", item.getVariable(DocumentRoutingConstants.TASK_NODE_ID_KEY)); 133 134 jg.writeArrayFieldStart(TARGET_DOCUMENT_IDS); 135 final boolean isFetchTargetDocumentIds = ctx.getFetched(ENTITY_TYPE).contains(FETCH_TARGET_DOCUMENT); 136 for (String docId : item.getTargetDocumentsIds()) { 137 IdRef idRef = new IdRef(docId); 138 if (wrapper.getSession().exists(idRef)) { 139 if (isFetchTargetDocumentIds) { 140 writeEntity(wrapper.getSession().getDocument(idRef), jg); 141 } else { 142 jg.writeStartObject(); 143 jg.writeStringField("id", docId); 144 jg.writeEndObject(); 145 } 146 } 147 } 148 jg.writeEndArray(); 149 150 jg.writeArrayFieldStart("actors"); 151 final boolean isFetchActors = ctx.getFetched(ENTITY_TYPE).contains(FETCH_ACTORS); 152 for (String actorId : item.getActors()) { 153 if (isFetchActors) { 154 if (actorId.startsWith(USER_PREFIX + SEPARATOR)) { 155 actorId = actorId.substring(USER_PREFIX.length() + SEPARATOR.length()); 156 NuxeoPrincipal user = userManager.getPrincipal(actorId); 157 if (user != null) { 158 writeEntity(user, jg); 159 continue; 160 } 161 } else if (actorId.startsWith(GROUP_PREFIX + SEPARATOR)) { 162 actorId = actorId.substring(GROUP_PREFIX.length() + SEPARATOR.length()); 163 NuxeoGroup group = userManager.getGroup(actorId); 164 if (group != null) { 165 writeEntity(group, jg); 166 continue; 167 } 168 } else { 169 NuxeoPrincipal user = userManager.getPrincipal(actorId); 170 if (user != null) { 171 writeEntity(user, jg); 172 continue; 173 } else { 174 NuxeoGroup group = userManager.getGroup(actorId); 175 if (group != null) { 176 writeEntity(group, jg); 177 continue; 178 } 179 } 180 } 181 } 182 jg.writeStartObject(); 183 jg.writeStringField("id", actorId); 184 jg.writeEndObject(); 185 } 186 jg.writeEndArray(); 187 188 jg.writeArrayFieldStart("comments"); 189 for (TaskComment comment : item.getComments()) { 190 jg.writeStartObject(); 191 jg.writeStringField("author", comment.getAuthor()); 192 jg.writeStringField("text", comment.getText()); 193 jg.writeStringField("date", DateParser.formatW3CDateTime(comment.getCreationDate().getTime())); 194 jg.writeEndObject(); 195 } 196 jg.writeEndArray(); 197 198 jg.writeFieldName("variables"); 199 jg.writeStartObject(); 200 // add nodeVariables 201 if (node != null) { 202 writeTaskVariables(node, jg, registry, ctx, schemaManager); 203 } 204 // add workflow variables 205 if (workflowInstance != null) { 206 writeWorkflowVariables(workflowInstance, node, jg, registry, ctx, schemaManager); 207 } 208 jg.writeEndObject(); 209 210 if (node != null) { 211 jg.writeFieldName("taskInfo"); 212 jg.writeStartObject(); 213 final ActionManager actionManager = Framework.getService(ActionManager.class); 214 jg.writeArrayFieldStart("taskActions"); 215 for (Button button : node.getTaskButtons()) { 216 if (StringUtils.isBlank(button.getFilter()) || actionManager.checkFilter(button.getFilter(), 217 createActionContext(wrapper.getSession()))) { 218 jg.writeStartObject(); 219 jg.writeStringField("name", button.getName()); 220 jg.writeStringField("url", ctx.getBaseUrl() + "api/v1/task/" + item.getDocument().getId() + "/" 221 + button.getName()); 222 jg.writeStringField("label", button.getLabel()); 223 jg.writeEndObject(); 224 } 225 } 226 jg.writeEndArray(); 227 228 jg.writeFieldName("layoutResource"); 229 jg.writeStartObject(); 230 jg.writeStringField("name", node.getTaskLayout()); 231 jg.writeStringField("url", 232 ctx.getBaseUrl() + "site/layout-manager/layouts/?layoutName=" + node.getTaskLayout()); 233 jg.writeEndObject(); 234 235 jg.writeArrayFieldStart("schemas"); 236 for (String schema : node.getDocument().getSchemas()) { 237 // TODO only keep functional schema once adaptation done 238 jg.writeStartObject(); 239 jg.writeStringField("name", schema); 240 jg.writeStringField("url", ctx.getBaseUrl() + "api/v1/config/schemas/" + schema); 241 jg.writeEndObject(); 242 } 243 jg.writeEndArray(); 244 245 jg.writeEndObject(); 246 } 247 } 248 249 } 250 251 protected void writeWorkflowInitiator(JsonGenerator jg, String workflowInitiator) throws IOException { 252 if (ctx.getFetched(ENTITY_TYPE).contains(FETCH_WORKFLOW_INITATIOR)) { 253 NuxeoPrincipal principal = userManager.getPrincipal(workflowInitiator); 254 if (principal != null) { 255 writeEntityField("workflowInitiator", principal, jg); 256 return; 257 } 258 } 259 jg.writeStringField("workflowInitiator", workflowInitiator); 260 } 261 262 protected static ActionContext createActionContext(CoreSession session) { 263 ActionContext actionContext = new ELActionContext(); 264 actionContext.setDocumentManager(session); 265 actionContext.setCurrentPrincipal((NuxeoPrincipal) session.getPrincipal()); 266 return actionContext; 267 } 268 269 /** 270 * @since 8.3 271 */ 272 public static void writeTaskVariables(GraphNode node, JsonGenerator jg, MarshallerRegistry registry, 273 RenderingContext ctx, SchemaManager schemaManager) throws IOException, JsonGenerationException { 274 if (node == null || node.getDocument() == null) { 275 return; 276 } 277 String facet = (String) node.getDocument().getPropertyValue(GraphNode.PROP_VARIABLES_FACET); 278 if (StringUtils.isNotBlank(facet)) { 279 280 CompositeType type = schemaManager.getFacet(facet); 281 if (type != null) { 282 boolean hasFacet = node.getDocument().hasFacet(facet); 283 284 Writer<Property> propertyWriter = registry.getWriter(ctx, Property.class, APPLICATION_JSON_TYPE); 285 // provides the current route to the property marshaller 286 try (Closeable resource = ctx.wrap() 287 .with(DocumentModelJsonWriter.ENTITY_TYPE, node.getDocument()) 288 .open()) { 289 for (Field f : type.getFields()) { 290 String name = f.getName().getLocalName(); 291 Property property = hasFacet ? node.getDocument().getProperty(name) : null; 292 OutputStream out = new OutputStreamWithJsonWriter(jg); 293 jg.writeFieldName(name); 294 propertyWriter.write(property, Property.class, Property.class, APPLICATION_JSON_TYPE, out); 295 } 296 } 297 } 298 } 299 } 300 301 /** 302 * @since 8.3 303 */ 304 public static void writeWorkflowVariables(DocumentRoute route, GraphNode node, JsonGenerator jg, 305 MarshallerRegistry registry, RenderingContext ctx, SchemaManager schemaManager) 306 throws IOException, JsonGenerationException { 307 String facet = (String) route.getDocument().getPropertyValue(GraphRoute.PROP_VARIABLES_FACET); 308 if (StringUtils.isNotBlank(facet)) { 309 310 CompositeType type = schemaManager.getFacet(facet); 311 if (type != null) { 312 313 final String transientSchemaName = DocumentRoutingConstants.GLOBAL_VAR_SCHEMA_PREFIX + node.getId(); 314 final Schema transientSchema = schemaManager.getSchema(transientSchemaName); 315 if (transientSchema == null) { 316 return; 317 } 318 319 boolean hasFacet = route.getDocument().hasFacet(facet); 320 321 Writer<Property> propertyWriter = registry.getWriter(ctx, Property.class, APPLICATION_JSON_TYPE); 322 // provides the current route to the property marshaller 323 try (Closeable resource = ctx.wrap() 324 .with(DocumentModelJsonWriter.ENTITY_TYPE, route.getDocument()) 325 .open()) { 326 for (Field f : type.getFields()) { 327 String name = f.getName().getLocalName(); 328 if (transientSchema.hasField(name)) { 329 Property property = hasFacet ? route.getDocument().getProperty(name) : null; 330 OutputStream out = new OutputStreamWithJsonWriter(jg); 331 jg.writeFieldName(name); 332 propertyWriter.write(property, Property.class, Property.class, APPLICATION_JSON_TYPE, out); 333 } 334 } 335 } 336 } 337 } 338 } 339}