001/*
002 * (C) Copyright 2014-2015 Nuxeo SA (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.lang.StringUtils;
034import org.codehaus.jackson.JsonGenerationException;
035import org.codehaus.jackson.JsonGenerator;
036import org.nuxeo.ecm.core.api.CoreSession;
037import org.nuxeo.ecm.core.api.IdRef;
038import org.nuxeo.ecm.core.api.NuxeoGroup;
039import org.nuxeo.ecm.core.api.NuxeoPrincipal;
040import org.nuxeo.ecm.core.api.model.Property;
041import org.nuxeo.ecm.core.io.marshallers.json.ExtensibleEntityJsonWriter;
042import org.nuxeo.ecm.core.io.marshallers.json.OutputStreamWithJsonWriter;
043import org.nuxeo.ecm.core.io.marshallers.json.document.DocumentModelJsonWriter;
044import org.nuxeo.ecm.core.io.registry.MarshallerRegistry;
045import org.nuxeo.ecm.core.io.registry.Writer;
046import org.nuxeo.ecm.core.io.registry.context.RenderingContext;
047import org.nuxeo.ecm.core.io.registry.context.RenderingContext.SessionWrapper;
048import org.nuxeo.ecm.core.io.registry.reflect.Setup;
049import org.nuxeo.ecm.core.schema.SchemaManager;
050import org.nuxeo.ecm.core.schema.types.CompositeType;
051import org.nuxeo.ecm.core.schema.types.Field;
052import org.nuxeo.ecm.core.schema.types.Schema;
053import org.nuxeo.ecm.core.schema.utils.DateParser;
054import org.nuxeo.ecm.platform.actions.ActionContext;
055import org.nuxeo.ecm.platform.actions.ELActionContext;
056import org.nuxeo.ecm.platform.actions.ejb.ActionManager;
057import org.nuxeo.ecm.platform.routing.api.DocumentRoute;
058import org.nuxeo.ecm.platform.routing.api.DocumentRoutingConstants;
059import org.nuxeo.ecm.platform.routing.core.impl.GraphNode;
060import org.nuxeo.ecm.platform.routing.core.impl.GraphNode.Button;
061import org.nuxeo.ecm.platform.routing.core.impl.GraphRoute;
062import org.nuxeo.ecm.platform.task.Task;
063import org.nuxeo.ecm.platform.task.TaskComment;
064import org.nuxeo.ecm.platform.usermanager.UserManager;
065import org.nuxeo.runtime.api.Framework;
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    protected static final String USER_PREFIX = "user";
080
081    protected static final String GROUP_PREFIX = "group";
082
083    protected static final String SEPARATOR = ":";
084
085    @Inject
086    private SchemaManager schemaManager;
087
088    @Inject
089    UserManager userManager;
090
091    public TaskWriter() {
092        super(ENTITY_TYPE, Task.class);
093    }
094
095    public static final String ENTITY_TYPE = "task";
096
097    @Override
098    public void writeEntityBody(Task item, JsonGenerator jg) throws IOException {
099        GraphRoute workflowInstance = null;
100        GraphNode node = null;
101        String workflowInstanceId = item.getProcessId();
102        final String nodeId = item.getVariable(DocumentRoutingConstants.TASK_NODE_ID_KEY);
103        try (SessionWrapper wrapper = ctx.getSession(item.getDocument())) {
104            if (StringUtils.isNotBlank(workflowInstanceId)) {
105                NodeAccessRunner nodeAccessRunner = new NodeAccessRunner(wrapper.getSession(), workflowInstanceId,
106                        nodeId);
107                nodeAccessRunner.runUnrestricted();
108                workflowInstance = nodeAccessRunner.getWorkflowInstance();
109                node = nodeAccessRunner.getNode();
110            }
111
112            jg.writeStringField("id", item.getDocument().getId());
113            jg.writeStringField("name", item.getName());
114            jg.writeStringField("workflowInstanceId", workflowInstanceId);
115            if (workflowInstance != null) {
116                jg.writeStringField("workflowModelName", workflowInstance.getModelName());
117            }
118            jg.writeStringField("state", item.getDocument().getCurrentLifeCycleState());
119            jg.writeStringField("directive", item.getDirective());
120            jg.writeStringField("created", DateParser.formatW3CDateTime(item.getCreated()));
121            jg.writeStringField("dueDate", DateParser.formatW3CDateTime(item.getDueDate()));
122            jg.writeStringField("nodeName", item.getVariable(DocumentRoutingConstants.TASK_NODE_ID_KEY));
123
124            jg.writeArrayFieldStart(TARGET_DOCUMENT_IDS);
125            final boolean isFetchTargetDocumentIds = ctx.getFetched(ENTITY_TYPE).contains(FETCH_TARGET_DOCUMENT);
126            for (String docId : item.getTargetDocumentsIds()) {
127                IdRef idRef = new IdRef(docId);
128                if (wrapper.getSession().exists(idRef)) {
129                    if (isFetchTargetDocumentIds) {
130                        writeEntity(wrapper.getSession().getDocument(idRef), jg);
131                    } else {
132                        jg.writeStartObject();
133                        jg.writeStringField("id", docId);
134                        jg.writeEndObject();
135                    }
136                }
137            }
138            jg.writeEndArray();
139
140            jg.writeArrayFieldStart("actors");
141            final boolean isFetchActors = ctx.getFetched(ENTITY_TYPE).contains(FETCH_ACTORS);
142            for (String actorId : item.getActors()) {
143                if (isFetchActors) {
144                    if (actorId.startsWith(USER_PREFIX + SEPARATOR)) {
145                        actorId = actorId.substring(USER_PREFIX.length() + SEPARATOR.length());
146                        NuxeoPrincipal user = userManager.getPrincipal(actorId);
147                        if (user != null) {
148                            writeEntity(user, jg);
149                            continue;
150                        }
151                    } else if (actorId.startsWith(GROUP_PREFIX + SEPARATOR)) {
152                        actorId = actorId.substring(GROUP_PREFIX.length() + SEPARATOR.length());
153                        NuxeoGroup group = userManager.getGroup(actorId);
154                        if (group != null) {
155                            writeEntity(group, jg);
156                            continue;
157                        }
158                    } else {
159                        NuxeoPrincipal user = userManager.getPrincipal(actorId);
160                        if (user != null) {
161                            writeEntity(user, jg);
162                            continue;
163                        } else {
164                            NuxeoGroup group = userManager.getGroup(actorId);
165                            if (group != null) {
166                                writeEntity(group, jg);
167                                continue;
168                            }
169                        }
170                    }
171                }
172                jg.writeStartObject();
173                jg.writeStringField("id", actorId);
174                jg.writeEndObject();
175            }
176            jg.writeEndArray();
177
178            jg.writeArrayFieldStart("comments");
179            for (TaskComment comment : item.getComments()) {
180                jg.writeStartObject();
181                jg.writeStringField("author", comment.getAuthor());
182                jg.writeStringField("text", comment.getText());
183                jg.writeStringField("date", DateParser.formatW3CDateTime(comment.getCreationDate().getTime()));
184                jg.writeEndObject();
185            }
186            jg.writeEndArray();
187
188            jg.writeFieldName("variables");
189            jg.writeStartObject();
190            // add nodeVariables
191            if (node != null) {
192                writeTaskVariables(node, jg, registry, ctx, schemaManager);
193            }
194            // add workflow variables
195            if (workflowInstance != null) {
196                writeWorkflowVariables(workflowInstance, node, jg, registry, ctx, schemaManager);
197            }
198            jg.writeEndObject();
199
200            if (node != null) {
201                jg.writeFieldName("taskInfo");
202                jg.writeStartObject();
203                final ActionManager actionManager = Framework.getService(ActionManager.class);
204                jg.writeArrayFieldStart("taskActions");
205                for (Button button : node.getTaskButtons()) {
206                    if (StringUtils.isBlank(button.getFilter()) || actionManager.checkFilter(button.getFilter(),
207                            createActionContext(wrapper.getSession()))) {
208                        jg.writeStartObject();
209                        jg.writeStringField("name", button.getName());
210                        jg.writeStringField("url", ctx.getBaseUrl() + "api/v1/task/" + item.getDocument().getId() + "/"
211                                + button.getName());
212                        jg.writeStringField("label", button.getLabel());
213                        jg.writeEndObject();
214                    }
215                }
216                jg.writeEndArray();
217
218                jg.writeFieldName("layoutResource");
219                jg.writeStartObject();
220                jg.writeStringField("name", node.getTaskLayout());
221                jg.writeStringField("url",
222                        ctx.getBaseUrl() + "site/layout-manager/layouts/?layoutName=" + node.getTaskLayout());
223                jg.writeEndObject();
224
225                jg.writeArrayFieldStart("schemas");
226                for (String schema : node.getDocument().getSchemas()) {
227                    // TODO only keep functional schema once adaptation done
228                    jg.writeStartObject();
229                    jg.writeStringField("name", schema);
230                    jg.writeStringField("url", ctx.getBaseUrl() + "api/v1/config/schemas/" + schema);
231                    jg.writeEndObject();
232                }
233                jg.writeEndArray();
234
235                jg.writeEndObject();
236            }
237        }
238
239    }
240
241    protected static ActionContext createActionContext(CoreSession session) {
242        ActionContext actionContext = new ELActionContext();
243        actionContext.setDocumentManager(session);
244        actionContext.setCurrentPrincipal((NuxeoPrincipal) session.getPrincipal());
245        return actionContext;
246    }
247
248    /**
249     * @since 8.3
250     */
251    public static void writeTaskVariables(GraphNode node, JsonGenerator jg, MarshallerRegistry registry,
252            RenderingContext ctx, SchemaManager schemaManager) throws IOException, JsonGenerationException {
253        if (node == null || node.getDocument() == null) {
254            return;
255        }
256        String facet = (String) node.getDocument().getPropertyValue(GraphNode.PROP_VARIABLES_FACET);
257        if (StringUtils.isNotBlank(facet)) {
258
259            CompositeType type = schemaManager.getFacet(facet);
260            if (type != null) {
261                boolean hasFacet = node.getDocument().hasFacet(facet);
262
263                Writer<Property> propertyWriter = registry.getWriter(ctx, Property.class, APPLICATION_JSON_TYPE);
264                // provides the current route to the property marshaller
265                try (Closeable resource = ctx.wrap()
266                                             .with(DocumentModelJsonWriter.ENTITY_TYPE, node.getDocument())
267                                             .open()) {
268                    for (Field f : type.getFields()) {
269                        String name = f.getName().getLocalName();
270                        Property property = hasFacet ? node.getDocument().getProperty(name) : null;
271                        OutputStream out = new OutputStreamWithJsonWriter(jg);
272                        jg.writeFieldName(name);
273                        propertyWriter.write(property, Property.class, Property.class, APPLICATION_JSON_TYPE, out);
274                    }
275                }
276            }
277        }
278    }
279
280    /**
281     * @since 8.3
282     */
283    public static void writeWorkflowVariables(DocumentRoute route, GraphNode node, JsonGenerator jg,
284            MarshallerRegistry registry, RenderingContext ctx, SchemaManager schemaManager)
285            throws IOException, JsonGenerationException {
286        String facet = (String) route.getDocument().getPropertyValue(GraphRoute.PROP_VARIABLES_FACET);
287        if (StringUtils.isNotBlank(facet)) {
288
289            CompositeType type = schemaManager.getFacet(facet);
290            if (type != null) {
291
292                final String transientSchemaName = DocumentRoutingConstants.GLOBAL_VAR_SCHEMA_PREFIX + node.getId();
293                final Schema transientSchema = schemaManager.getSchema(transientSchemaName);
294                if (transientSchema == null) {
295                    return;
296                }
297
298                boolean hasFacet = route.getDocument().hasFacet(facet);
299
300                Writer<Property> propertyWriter = registry.getWriter(ctx, Property.class, APPLICATION_JSON_TYPE);
301                // provides the current route to the property marshaller
302                try (Closeable resource = ctx.wrap()
303                                             .with(DocumentModelJsonWriter.ENTITY_TYPE, route.getDocument())
304                                             .open()) {
305                    for (Field f : type.getFields()) {
306                        String name = f.getName().getLocalName();
307                        if (transientSchema.hasField(name)) {
308                            Property property = hasFacet ? route.getDocument().getProperty(name) : null;
309                            OutputStream out = new OutputStreamWithJsonWriter(jg);
310                            jg.writeFieldName(name);
311                            propertyWriter.write(property, Property.class, Property.class, APPLICATION_JSON_TYPE, out);
312                        }
313                    }
314                }
315            }
316        }
317    }
318}