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