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}