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}