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}