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