001/*
002 * (C) Copyright 2007 Nuxeo SAS (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 *     Nuxeo - initial API and implementation
016 *
017 * $Id$
018 */
019
020package org.nuxeo.ecm.webapp.edit.lock;
021
022import static org.jboss.seam.annotations.Install.FRAMEWORK;
023import static org.nuxeo.ecm.core.api.security.SecurityConstants.EVERYTHING;
024import static org.nuxeo.ecm.core.api.security.SecurityConstants.WRITE_PROPERTIES;
025
026import java.io.Serializable;
027import java.text.DateFormat;
028import java.util.Date;
029import java.util.HashMap;
030import java.util.List;
031import java.util.Map;
032
033import org.apache.commons.lang.StringUtils;
034import org.apache.commons.logging.Log;
035import org.apache.commons.logging.LogFactory;
036import org.jboss.seam.ScopeType;
037import org.jboss.seam.annotations.Factory;
038import org.jboss.seam.annotations.In;
039import org.jboss.seam.annotations.Install;
040import org.jboss.seam.annotations.Name;
041import org.jboss.seam.annotations.Observer;
042import org.jboss.seam.annotations.Scope;
043import org.jboss.seam.annotations.intercept.BypassInterceptors;
044import org.jboss.seam.contexts.Context;
045import org.jboss.seam.contexts.Contexts;
046import org.jboss.seam.core.Events;
047import org.jboss.seam.faces.FacesMessages;
048import org.jboss.seam.international.StatusMessage;
049import org.nuxeo.ecm.core.api.CoreSession;
050import org.nuxeo.ecm.core.api.DocumentModel;
051import org.nuxeo.ecm.core.api.DocumentRef;
052import org.nuxeo.ecm.core.api.Lock;
053import org.nuxeo.ecm.core.api.NuxeoPrincipal;
054import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner;
055import org.nuxeo.ecm.platform.actions.Action;
056import org.nuxeo.ecm.platform.ui.web.api.NavigationContext;
057import org.nuxeo.ecm.platform.ui.web.api.WebActions;
058import org.nuxeo.ecm.webapp.helpers.EventNames;
059import org.nuxeo.ecm.webapp.helpers.ResourcesAccessor;
060
061/**
062 * This is the action listener that knows to decide if an user has the right to take the lock or release the lock of a
063 * document.
064 * <p>
065 * Most of the logic of this bean should either be moved into a DocumentModel adapter or directly into the core API.
066 *
067 * @author <a href="mailto:bt@nuxeo.com">Bogdan Tatar</a>
068 */
069@Name("lockActions")
070@Scope(ScopeType.EVENT)
071@Install(precedence = FRAMEWORK)
072public class LockActionsBean implements LockActions {
073    // XXX: OG: How a remote calls could possibly work without the seam
074    // injected
075    // components??
076
077    private static final long serialVersionUID = -8050964269646803077L;
078
079    private static final Log log = LogFactory.getLog(LockActionsBean.class);
080
081    private static final String EDIT_ACTIONS = "EDIT_ACTIONS";
082
083    @In
084    private transient NavigationContext navigationContext;
085
086    @In(create = true, required = false)
087    protected transient FacesMessages facesMessages;
088
089    @In(create = true)
090    protected transient ResourcesAccessor resourcesAccessor;
091
092    @In(create = true)
093    protected transient WebActions webActions;
094
095    @In(create = true, required = false)
096    protected transient CoreSession documentManager;
097
098    // cache lock details states to reduce costly core session remote calls
099    private Map<String, Serializable> lockDetails;
100
101    private String documentId;
102
103    @Override
104    public Boolean getCanLockDoc(DocumentModel document) {
105        boolean canLock;
106        if (document == null) {
107            log.warn("Can't evaluate lock action : currentDocument is null");
108            canLock = false;
109        } else if (document.isProxy()) {
110            canLock = false;
111        } else {
112            NuxeoPrincipal userName = (NuxeoPrincipal) documentManager.getPrincipal();
113            Lock lock = documentManager.getLockInfo(document.getRef());
114            canLock = lock == null
115                    && (userName.isAdministrator() || isManagerOnDocument(document.getRef())
116                            || documentManager.hasPermission(document.getRef(), WRITE_PROPERTIES))
117                    && !document.isVersion();
118        }
119        return canLock;
120    }
121
122    protected boolean isManagerOnDocument(DocumentRef ref) {
123        return documentManager.hasPermission(ref, EVERYTHING);
124    }
125
126    @Override
127    @Factory(value = "currentDocumentCanBeLocked", scope = ScopeType.EVENT)
128    public Boolean getCanLockCurrentDoc() {
129        DocumentModel currentDocument = navigationContext.getCurrentDocument();
130        return getCanLockDoc(currentDocument);
131    }
132
133    @Observer(value = { EventNames.USER_ALL_DOCUMENT_TYPES_SELECTION_CHANGED }, create = false)
134    @BypassInterceptors
135    public void resetEventContext() {
136        Context evtCtx = Contexts.getEventContext();
137        if (evtCtx != null) {
138            evtCtx.remove("currentDocumentCanBeLocked");
139            evtCtx.remove("currentDocumentLockDetails");
140            evtCtx.remove("currentDocumentCanBeUnlocked");
141        }
142    }
143
144    @Override
145    public Boolean getCanUnlockDoc(DocumentModel document) {
146        boolean canUnlock = false;
147        if (document == null) {
148            canUnlock = false;
149        } else {
150            NuxeoPrincipal userName = (NuxeoPrincipal) documentManager.getPrincipal();
151            Map<String, Serializable> lockDetails = getLockDetails(document);
152            if (lockDetails.isEmpty() || document.isProxy()) {
153                canUnlock = false;
154            } else {
155                canUnlock = ((userName.isAdministrator()
156                        || documentManager.hasPermission(document.getRef(), EVERYTHING))
157                                ? true
158                                : (userName.getName().equals(lockDetails.get(LOCKER))
159                                        && documentManager.hasPermission(document.getRef(), WRITE_PROPERTIES)))
160                        && !document.isVersion();
161            }
162        }
163        return canUnlock;
164    }
165
166    @Override
167    @Factory(value = "currentDocumentCanBeUnlocked", scope = ScopeType.EVENT)
168    public Boolean getCanUnlockCurrentDoc() {
169        DocumentModel currentDocument = navigationContext.getCurrentDocument();
170        return getCanUnlockDoc(currentDocument);
171    }
172
173    @Override
174    public String lockCurrentDocument() {
175        String view = lockDocument(navigationContext.getCurrentDocument());
176        navigationContext.invalidateCurrentDocument();
177        return view;
178    }
179
180    @Override
181    public String lockDocument(DocumentModel document) {
182        log.debug("Lock a document ...");
183        resetEventContext();
184        String message = "document.lock.failed";
185        DocumentRef ref = document.getRef();
186        if (documentManager.hasPermission(ref, WRITE_PROPERTIES) && documentManager.getLockInfo(ref) == null) {
187            documentManager.setLock(ref);
188            documentManager.save();
189            message = "document.lock";
190            Events.instance().raiseEvent(EventNames.DOCUMENT_LOCKED, document);
191            Events.instance().raiseEvent(EventNames.DOCUMENT_CHANGED, document);
192        }
193        facesMessages.add(StatusMessage.Severity.INFO, resourcesAccessor.getMessages().get(message));
194        resetLockState();
195        webActions.resetTabList();
196        return null;
197    }
198
199    @Override
200    public String unlockCurrentDocument() {
201        String view = unlockDocument(navigationContext.getCurrentDocument());
202        navigationContext.invalidateCurrentDocument();
203        return view;
204    }
205
206    // helper inner class to do the unrestricted unlock
207    protected class UnrestrictedUnlocker extends UnrestrictedSessionRunner {
208
209        private final DocumentRef docRefToUnlock;
210
211        protected UnrestrictedUnlocker(DocumentRef docRef) {
212            super(documentManager);
213            docRefToUnlock = docRef;
214        }
215
216        /*
217         * Use an unrestricted session to unlock the document.
218         */
219        @Override
220        public void run() {
221            session.removeLock(docRefToUnlock);
222            session.save();
223        }
224    }
225
226    @Override
227    public String unlockDocument(DocumentModel document) {
228        log.debug("Unlock a document ...");
229        resetEventContext();
230        String message;
231        Map<String, Serializable> lockDetails = getLockDetails(document);
232        if (lockDetails == null) {
233            message = "document.unlock.done";
234        } else {
235            NuxeoPrincipal userName = (NuxeoPrincipal) documentManager.getPrincipal();
236            if (userName.isAdministrator() || documentManager.hasPermission(document.getRef(), EVERYTHING)
237                    || userName.getName().equals(lockDetails.get(LOCKER))) {
238
239                if (!documentManager.hasPermission(document.getRef(), WRITE_PROPERTIES)) {
240                    // Here administrator should always be able to unlock so
241                    // we need to grant him this possibility even if it
242                    // doesn't have the write permission.
243
244                    new UnrestrictedUnlocker(document.getRef()).runUnrestricted();
245
246                    documentManager.save(); // process invalidations from unrestricted session
247
248                    message = "document.unlock";
249                } else {
250                    documentManager.removeLock(document.getRef());
251                    documentManager.save();
252                    message = "document.unlock";
253                }
254                Events.instance().raiseEvent(EventNames.DOCUMENT_UNLOCKED, document);
255                Events.instance().raiseEvent(EventNames.DOCUMENT_CHANGED, document);
256            } else {
257                message = "document.unlock.not.permitted";
258            }
259        }
260        facesMessages.add(StatusMessage.Severity.INFO, resourcesAccessor.getMessages().get(message));
261        resetLockState();
262        webActions.resetTabList();
263        return null;
264    }
265
266    @Override
267    public Action getLockOrUnlockAction() {
268        log.debug("Get lock or unlock action ...");
269        Action lockOrUnlockAction = null;
270        List<Action> actionsList = webActions.getActionsList(EDIT_ACTIONS);
271        if (actionsList != null && !actionsList.isEmpty()) {
272            lockOrUnlockAction = actionsList.get(0);
273        }
274        return lockOrUnlockAction;
275    }
276
277    @Override
278    @Factory(value = "currentDocumentLockDetails", scope = ScopeType.EVENT)
279    public Map<String, Serializable> getCurrentDocLockDetails() {
280        Map<String, Serializable> details = null;
281        if (navigationContext.getCurrentDocument() != null) {
282            details = getLockDetails(navigationContext.getCurrentDocument());
283        }
284        return details;
285    }
286
287    @Override
288    public Map<String, Serializable> getLockDetails(DocumentModel document) {
289        if (lockDetails == null || !StringUtils.equals(documentId, document.getId())) {
290            lockDetails = new HashMap<String, Serializable>();
291            documentId = document.getId();
292            Lock lock = documentManager.getLockInfo(document.getRef());
293            if (lock == null) {
294                return lockDetails;
295            }
296            lockDetails.put(LOCKER, lock.getOwner());
297            lockDetails.put(LOCK_CREATED, lock.getCreated());
298            lockDetails.put(LOCK_TIME,
299                    DateFormat.getDateInstance(DateFormat.MEDIUM).format(new Date(lock.getCreated().getTimeInMillis())));
300        }
301        return lockDetails;
302    }
303
304    @Override
305    @BypassInterceptors
306    public void resetLockState() {
307        lockDetails = null;
308        documentId = null;
309    }
310
311}