001/*
002 * (C) Copyright 2009 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 *     Thomas Roger
018 */
019
020package org.nuxeo.ecm.platform.publisher.task;
021
022import java.io.Serializable;
023import java.util.Arrays;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027
028import org.nuxeo.ecm.core.api.CoreSession;
029import org.nuxeo.ecm.core.api.DocumentModel;
030import org.nuxeo.ecm.core.api.DocumentModelList;
031import org.nuxeo.ecm.core.api.DocumentRef;
032import org.nuxeo.ecm.core.api.IdRef;
033import org.nuxeo.ecm.core.api.NuxeoPrincipal;
034import org.nuxeo.ecm.core.api.PathRef;
035import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner;
036import org.nuxeo.ecm.core.api.security.ACE;
037import org.nuxeo.ecm.core.api.security.ACL;
038import org.nuxeo.ecm.core.api.security.ACP;
039import org.nuxeo.ecm.core.api.security.SecurityConstants;
040import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
041import org.nuxeo.ecm.platform.ec.notification.NotificationConstants;
042import org.nuxeo.ecm.platform.publisher.api.PublicationNode;
043import org.nuxeo.ecm.platform.publisher.api.PublishedDocument;
044import org.nuxeo.ecm.platform.publisher.api.PublishedDocumentFactory;
045import org.nuxeo.ecm.platform.publisher.api.PublishingEvent;
046import org.nuxeo.ecm.platform.publisher.impl.core.CoreFolderPublicationNode;
047import org.nuxeo.ecm.platform.publisher.impl.core.CoreProxyFactory;
048import org.nuxeo.ecm.platform.publisher.impl.core.SimpleCorePublishedDocument;
049import org.nuxeo.ecm.platform.publisher.rules.ValidatorsRule;
050import org.nuxeo.ecm.platform.task.Task;
051import org.nuxeo.ecm.platform.task.TaskEventNames;
052import org.nuxeo.ecm.platform.task.TaskService;
053import org.nuxeo.runtime.api.Framework;
054
055/**
056 * Implementation of the {@link PublishedDocumentFactory} for core implementation using native proxy system with
057 * validation workflow.
058 *
059 * @author <a href="mailto:troger@nuxeo.com">Thomas Roger</a>
060 * @author <a href="mailto:tmartins@nuxeo.com">Thierry Martins</a>
061 * @author <a href="mailto:ataillefer@nuxeo.com">Antoine Taillefer</a>
062 */
063public class CoreProxyWithWorkflowFactory extends CoreProxyFactory implements PublishedDocumentFactory {
064
065    public static final String TASK_NAME = "org.nuxeo.ecm.platform.publisher.task.CoreProxyWithWorkflowFactory";
066
067    public static final String ACL_NAME = "org.nuxeo.ecm.platform.publisher.task.CoreProxyWithWorkflowFactory";
068
069    // XXX ataillefer: remove if refactor old JBPM ACL name
070    public static final String JBPM_ACL_NAME = "org.nuxeo.ecm.platform.publisher.jbpm.CoreProxyWithWorkflowFactory";
071
072    public static final String PUBLISH_TASK_TYPE = "publish_moderate";
073
074    public static final String LOOKUP_STATE_PARAM_KEY = "lookupState";
075
076    public static final String LOOKUP_STATE_PARAM_BYACL = "byACL";
077
078    public static final String LOOKUP_STATE_PARAM_BYTASK = "byTask";
079
080    protected LookupState lookupState = new LookupStateByACL();
081
082    @Override
083    public void init(CoreSession coreSession, ValidatorsRule validatorsRule, Map<String, String> parameters) {
084        super.init(coreSession, validatorsRule, parameters);
085        // setup lookup state strategy if requested
086        String lookupState = parameters.get(LOOKUP_STATE_PARAM_KEY);
087        if (lookupState != null) {
088            if (LOOKUP_STATE_PARAM_BYACL.equals(lookupState)) {
089                setLookupByACL();
090            } else if (LOOKUP_STATE_PARAM_BYTASK.equals(lookupState)) {
091                setLookupByTask();
092            }
093        }
094    }
095
096    public void setLookupByTask() {
097        this.lookupState = new LookupStateByTask();
098    }
099
100    public void setLookupByACL() {
101        this.lookupState = new LookupStateByACL();
102    }
103
104    @Override
105    public PublishedDocument publishDocument(DocumentModel doc, PublicationNode targetNode,
106            Map<String, String> params) {
107        DocumentModel targetDocModel;
108        if (targetNode instanceof CoreFolderPublicationNode) {
109            CoreFolderPublicationNode coreNode = (CoreFolderPublicationNode) targetNode;
110            targetDocModel = coreNode.getTargetDocumentModel();
111        } else {
112            targetDocModel = coreSession.getDocument(new PathRef(targetNode.getPath()));
113        }
114        NuxeoPrincipal principal = (NuxeoPrincipal) coreSession.getPrincipal();
115        DocumentPublisherUnrestricted runner = new DocumentPublisherUnrestricted(coreSession, doc.getRef(),
116                targetDocModel.getRef(), principal, null);
117        runner.runUnrestricted();
118        return runner.getPublishedDocument();
119    }
120
121    protected boolean isPublishedDocWaitingForPublication(DocumentModel doc, CoreSession session) {
122        return !lookupState.isPublished(doc, session);
123    }
124
125    protected boolean isValidator(DocumentModel document, NuxeoPrincipal principal) {
126        String[] validators = getValidatorsFor(document);
127        for (String s : validators) {
128            if (principal.getName().equals(s) || principal.isMemberOf(s)) {
129                return true;
130            }
131        }
132        return false;
133    }
134
135    protected void restrictPermission(DocumentModel newProxy, NuxeoPrincipal principal, CoreSession coreSession,
136            ACL acl) {
137        ChangePermissionUnrestricted permissionChanger = new ChangePermissionUnrestricted(coreSession, newProxy,
138                getValidatorsFor(newProxy), principal, ACL_NAME, acl);
139        permissionChanger.runUnrestricted();
140    }
141
142    protected void createTask(DocumentModel document, CoreSession session, NuxeoPrincipal principal) {
143        String[] actorIds = getValidatorsFor(document);
144        Map<String, String> variables = new HashMap<String, String>();
145        variables.put(Task.TaskVariableName.needi18n.name(), "true");
146        variables.put(Task.TaskVariableName.taskType.name(), PUBLISH_TASK_TYPE);
147        variables.put(TaskService.VariableName.documentId.name(), document.getId());
148        variables.put(TaskService.VariableName.documentRepositoryName.name(), document.getRepositoryName());
149        variables.put(TaskService.VariableName.initiator.name(), principal.getName());
150
151        getTaskService().createTask(session, principal, document, TASK_NAME, Arrays.asList(actorIds), false, TASK_NAME,
152                null, null, variables, null);
153        DocumentEventContext ctx = new DocumentEventContext(session, principal, document);
154        ctx.setProperty(NotificationConstants.RECIPIENTS_KEY, actorIds);
155        getEventProducer().fireEvent(ctx.newEvent(TaskEventNames.WORKFLOW_TASK_START));
156    }
157
158    protected TaskService getTaskService() {
159        return Framework.getService(TaskService.class);
160    }
161
162    protected void removeExistingProxiesOnPreviousVersions(DocumentModel newProxy) {
163        new UnrestrictedSessionRunner(coreSession) {
164            @Override
165            public void run() {
166                DocumentModel sourceVersion = session.getSourceDocument(newProxy.getRef());
167                DocumentModel dm = session.getSourceDocument(sourceVersion.getRef());
168                DocumentModelList brothers = session.getProxies(dm.getRef(), newProxy.getParentRef());
169                if (brothers != null && brothers.size() > 1) {
170                    // we remove the brothers of the published document if any
171                    // the use case is:
172                    // v1 is published, v2 is waiting for publication and was just
173                    // validated
174                    // v1 is removed and v2 is now being published
175                    for (DocumentModel doc : brothers) {
176                        if (!doc.getId().equals(newProxy.getId())) {
177                            session.removeDocument(doc.getRef());
178                        }
179                    }
180                }
181            }
182        }.runUnrestricted();
183    }
184
185    @Override
186    public void validatorPublishDocument(PublishedDocument publishedDocument, String comment) {
187        DocumentModel proxy = ((SimpleCorePublishedDocument) publishedDocument).getProxy();
188        NuxeoPrincipal principal = (NuxeoPrincipal) coreSession.getPrincipal();
189        removeExistingProxiesOnPreviousVersions(proxy);
190        removeACL(proxy, coreSession);
191        endTask(proxy, principal, coreSession, comment, PublishingEvent.documentPublicationApproved);
192        notifyEvent(PublishingEvent.documentPublicationApproved, proxy, coreSession);
193        notifyEvent(PublishingEvent.documentPublished, proxy, coreSession);
194        ((SimpleCorePublishedDocument) publishedDocument).setPending(false);
195    }
196
197    protected void removeACL(DocumentModel document, CoreSession coreSession) {
198        RemoveACLUnrestricted remover = new RemoveACLUnrestricted(coreSession, document, ACL_NAME, JBPM_ACL_NAME);
199        remover.runUnrestricted();
200    }
201
202    protected void endTask(DocumentModel document, NuxeoPrincipal currentUser, CoreSession session, String comment,
203            PublishingEvent event) {
204        List<Task> tis = getTaskService().getTaskInstances(document, currentUser, session);
205        String initiator = null;
206        for (Task task : tis) {
207            if (task.getName().equals(TASK_NAME)) {
208                initiator = (String) task.getVariable(TaskService.VariableName.initiator.name());
209                task.end(session);
210                // make sure taskDoc is attached to prevent sending event with null session
211                DocumentModel taskDocument = task.getDocument();
212                if (taskDocument.getSessionId() == null) {
213                    taskDocument.attach(coreSession.getSessionId());
214                }
215                session.saveDocument(taskDocument);
216                break;
217            }
218        }
219        DocumentModel liveDoc = getLiveDocument(session, document);
220        Map<String, Serializable> properties = new HashMap<String, Serializable>();
221        if (initiator != null) {
222            properties.put(NotificationConstants.RECIPIENTS_KEY, new String[] { initiator });
223        }
224        notifyEvent(event.name(), properties, comment, null, liveDoc, session);
225    }
226
227    protected DocumentModel getLiveDocument(CoreSession session, DocumentModel proxy) {
228        GetsProxySourceDocumentsUnrestricted runner = new GetsProxySourceDocumentsUnrestricted(session, proxy);
229        runner.runUnrestricted();
230        return runner.liveDocument;
231    }
232
233    @Override
234    public void validatorRejectPublication(PublishedDocument publishedDocument, String comment) {
235        DocumentModel proxy = ((SimpleCorePublishedDocument) publishedDocument).getProxy();
236        NuxeoPrincipal principal = (NuxeoPrincipal) coreSession.getPrincipal();
237        notifyEvent(PublishingEvent.documentPublicationRejected, proxy, coreSession);
238        endTask(proxy, principal, coreSession, comment, PublishingEvent.documentPublicationRejected);
239        removeProxy(proxy, coreSession);
240    }
241
242    protected void removeProxy(DocumentModel doc, CoreSession coreSession) {
243        DeleteDocumentUnrestricted deleter = new DeleteDocumentUnrestricted(coreSession, doc);
244        deleter.runUnrestricted();
245    }
246
247    @Override
248    public PublishedDocument wrapDocumentModel(DocumentModel doc) {
249        final SimpleCorePublishedDocument publishedDocument = (SimpleCorePublishedDocument) super.wrapDocumentModel(
250                doc);
251
252        new UnrestrictedSessionRunner(coreSession) {
253            @Override
254            public void run() {
255                if (!isPublished(publishedDocument, session)) {
256                    publishedDocument.setPending(true);
257                }
258
259            }
260        }.runUnrestricted();
261
262        return publishedDocument;
263    }
264
265    protected boolean isPublished(PublishedDocument publishedDocument, CoreSession session) {
266        // FIXME: should be cached
267        DocumentModel proxy = ((SimpleCorePublishedDocument) publishedDocument).getProxy();
268        return lookupState.isPublished(proxy, session);
269    }
270
271    @Override
272    public boolean canManagePublishing(PublishedDocument publishedDocument) {
273        DocumentModel proxy = ((SimpleCorePublishedDocument) publishedDocument).getProxy();
274        NuxeoPrincipal currentUser = (NuxeoPrincipal) coreSession.getPrincipal();
275        return proxy.isProxy() && hasValidationTask(proxy, currentUser);
276    }
277
278    protected boolean hasValidationTask(DocumentModel proxy, NuxeoPrincipal currentUser) {
279        assert currentUser != null;
280        List<Task> tasks = getTaskService().getTaskInstances(proxy, currentUser, coreSession);
281        for (Task task : tasks) {
282            if (task.getName().equals(TASK_NAME)) {
283                return true;
284            }
285        }
286        return false;
287    }
288
289    @Override
290    public boolean hasValidationTask(PublishedDocument publishedDocument) {
291        DocumentModel proxy = ((SimpleCorePublishedDocument) publishedDocument).getProxy();
292        NuxeoPrincipal currentUser = (NuxeoPrincipal) coreSession.getPrincipal();
293        return hasValidationTask(proxy, currentUser);
294    }
295
296    private class GetsProxySourceDocumentsUnrestricted extends UnrestrictedSessionRunner {
297
298        public DocumentModel liveDocument;
299
300        private DocumentModel sourceDocument;
301
302        private final DocumentModel document;
303
304        public GetsProxySourceDocumentsUnrestricted(CoreSession session, DocumentModel proxy) {
305            super(session);
306            this.document = proxy;
307        }
308
309        @Override
310        public void run() {
311            sourceDocument = session.getDocument(new IdRef(document.getSourceId()));
312            liveDocument = session.getDocument(new IdRef(sourceDocument.getSourceId()));
313        }
314    }
315
316    /**
317     * @author arussel
318     */
319    protected class DocumentPublisherUnrestricted extends UnrestrictedSessionRunner {
320
321        protected PublishedDocument result;
322
323        protected DocumentRef docRef;
324
325        protected DocumentRef targetRef;
326
327        protected NuxeoPrincipal principal;
328
329        protected String comment = "";
330
331        public DocumentPublisherUnrestricted(CoreSession session, DocumentRef docRef, DocumentRef targetRef,
332                NuxeoPrincipal principal, String comment) {
333            super(session);
334            this.docRef = docRef;
335            this.targetRef = targetRef;
336            this.principal = principal;
337            this.comment = comment;
338        }
339
340        public PublishedDocument getPublishedDocument() {
341            return result;
342        }
343
344        @Override
345        public void run() {
346            DocumentModelList list = session.getProxies(docRef, targetRef);
347            DocumentModel proxy = null;
348            if (list.isEmpty()) {// first publication
349                proxy = session.publishDocument(session.getDocument(docRef), session.getDocument(targetRef));
350                SimpleCorePublishedDocument publishedDocument = new SimpleCorePublishedDocument(proxy);
351                session.save();
352                if (!isValidator(proxy, principal)) {
353                    notifyEvent(PublishingEvent.documentWaitingPublication, coreSession.getDocument(proxy.getRef()),
354                            coreSession);
355                    restrictPermission(proxy, principal, session, null);
356                    createTask(proxy, coreSession, principal);
357                    publishedDocument.setPending(true);
358                } else {
359                    notifyEvent(PublishingEvent.documentPublished, proxy, coreSession);
360                }
361                result = publishedDocument;
362            } else if (list.size() == 1) {
363                // one doc is already published or waiting for publication
364                if (isPublishedDocWaitingForPublication(list.get(0), session)) {
365                    proxy = session.publishDocument(session.getDocument(docRef), session.getDocument(targetRef));
366                    if (!isValidator(proxy, principal)) {
367                        // we're getting the old proxy acl
368                        ACL acl = list.get(0).getACP().getACL(ACL_NAME);
369                        acl.add(0, new ACE(principal.getName(), SecurityConstants.READ, true));
370                        ACP acp = proxy.getACP();
371                        acp.addACL(acl);
372                        session.setACP(proxy.getRef(), acp, true);
373                        session.save();
374                        SimpleCorePublishedDocument publishedDocument = new SimpleCorePublishedDocument(proxy);
375                        publishedDocument.setPending(true);
376                        result = publishedDocument;
377                    } else {
378                        endTask(proxy, principal, coreSession, "", PublishingEvent.documentPublicationApproved);
379                        notifyEvent(PublishingEvent.documentPublished, proxy, coreSession);
380                        ACP acp = proxy.getACP();
381                        acp.removeACL(ACL_NAME);
382                        session.setACP(proxy.getRef(), acp, true);
383                        session.save();
384                        result = new SimpleCorePublishedDocument(proxy);
385                    }
386                } else {
387                    if (!isValidator(list.get(0), principal)) {
388                        proxy = session.publishDocument(session.getDocument(docRef), session.getDocument(targetRef),
389                                false);
390                        // save needed to have the proxy visible from other
391                        // sessions in non-JCA mode
392                        session.save();
393                        SimpleCorePublishedDocument publishedDocument = new SimpleCorePublishedDocument(proxy);
394                        notifyEvent(PublishingEvent.documentWaitingPublication, proxy, coreSession);
395                        restrictPermission(proxy, principal, coreSession, null);
396                        session.save(); // process invalidations (non-JCA)
397                        createTask(proxy, coreSession, principal);
398                        publishedDocument.setPending(true);
399                        result = publishedDocument;
400                    } else {
401                        proxy = session.publishDocument(session.getDocument(docRef), session.getDocument(targetRef));
402                        // save needed to have the proxy visible from other
403                        // sessions in non-JCA mode
404                        session.save();
405                        notifyEvent(PublishingEvent.documentPublished, proxy, coreSession);
406                        SimpleCorePublishedDocument publishedDocument = new SimpleCorePublishedDocument(proxy);
407                        result = publishedDocument;
408                    }
409                }
410            } else if (list.size() == 2) {
411                DocumentModel waitingForPublicationDoc = null;
412                session.save(); // process invalidations (non-JCA)
413                for (DocumentModel dm : list) {
414                    if (session.getACP(dm.getRef()).getACL(ACL_NAME) != null) {
415                        waitingForPublicationDoc = dm;
416                    }
417                }
418                if (!isValidator(waitingForPublicationDoc, principal)) {
419                    // we're getting the old proxy acl
420                    ACL acl = waitingForPublicationDoc.getACP().getACL(ACL_NAME);
421                    acl.add(0, new ACE(principal.getName(), SecurityConstants.READ, true));
422                    // remove publishedDoc
423                    ACP acp = session.getACP(waitingForPublicationDoc.getRef());
424                    acp.addACL(acl);
425                    session.setACP(waitingForPublicationDoc.getRef(), acp, true);
426                    SimpleCorePublishedDocument publishedDocument = new SimpleCorePublishedDocument(
427                            waitingForPublicationDoc);
428                    publishedDocument.setPending(true);
429                    result = publishedDocument;
430                } else {
431                    endTask(waitingForPublicationDoc, principal, coreSession, comment,
432                            PublishingEvent.documentPublicationApproved);
433                    session.removeDocument(waitingForPublicationDoc.getRef());
434                    proxy = session.publishDocument(session.getDocument(docRef), session.getDocument(targetRef));
435                    notifyEvent(PublishingEvent.documentPublished, proxy, coreSession);
436                    SimpleCorePublishedDocument publishedDocument = new SimpleCorePublishedDocument(proxy);
437                    result = publishedDocument;
438                }
439            }
440            if (proxy != null) {
441                proxy.detach(true);
442            }
443        }
444    }
445
446}