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}