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