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