001/* 002 * (C) Copyright 2006-2012 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 */ 020 021package org.nuxeo.ecm.webapp.contentbrowser; 022 023import static org.jboss.seam.ScopeType.CONVERSATION; 024import static org.jboss.seam.ScopeType.EVENT; 025 026import java.io.IOException; 027import java.io.Serializable; 028import java.util.HashMap; 029import java.util.List; 030import java.util.Map; 031 032import javax.faces.context.FacesContext; 033 034import org.apache.commons.logging.Log; 035import org.apache.commons.logging.LogFactory; 036import org.jboss.seam.annotations.Factory; 037import org.jboss.seam.annotations.In; 038import org.jboss.seam.annotations.Name; 039import org.jboss.seam.annotations.Observer; 040import org.jboss.seam.annotations.Scope; 041import org.jboss.seam.annotations.remoting.WebRemote; 042import org.jboss.seam.annotations.web.RequestParameter; 043import org.jboss.seam.core.Events; 044import org.jboss.seam.international.StatusMessage; 045import org.nuxeo.ecm.core.api.Blob; 046import org.nuxeo.ecm.core.api.CoreSession; 047import org.nuxeo.ecm.core.api.DocumentLocation; 048import org.nuxeo.ecm.core.api.DocumentModel; 049import org.nuxeo.ecm.core.api.DocumentRef; 050import org.nuxeo.ecm.core.api.IdRef; 051import org.nuxeo.ecm.core.api.blobholder.BlobHolder; 052import org.nuxeo.ecm.core.api.event.CoreEventConstants; 053import org.nuxeo.ecm.core.api.pathsegment.PathSegmentService; 054import org.nuxeo.ecm.core.api.security.SecurityConstants; 055import org.nuxeo.ecm.core.api.validation.DocumentValidationException; 056import org.nuxeo.ecm.core.blob.BlobManager; 057import org.nuxeo.ecm.core.blob.BlobProvider; 058import org.nuxeo.ecm.core.blob.ManagedBlob; 059import org.nuxeo.ecm.core.blob.apps.AppLink; 060import org.nuxeo.ecm.core.io.download.DownloadService; 061import org.nuxeo.ecm.core.schema.FacetNames; 062import org.nuxeo.ecm.platform.actions.Action; 063import org.nuxeo.ecm.platform.actions.ActionContext; 064import org.nuxeo.ecm.platform.types.Type; 065import org.nuxeo.ecm.platform.ui.web.api.NavigationContext; 066import org.nuxeo.ecm.platform.ui.web.api.UserAction; 067import org.nuxeo.ecm.platform.ui.web.api.WebActions; 068import org.nuxeo.ecm.platform.ui.web.tag.fn.Functions; 069import org.nuxeo.ecm.platform.ui.web.util.BaseURL; 070import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils; 071import org.nuxeo.ecm.platform.url.api.DocumentView; 072import org.nuxeo.ecm.platform.url.codec.DocumentFileCodec; 073import org.nuxeo.ecm.platform.util.RepositoryLocation; 074import org.nuxeo.ecm.webapp.action.ActionContextProvider; 075import org.nuxeo.ecm.webapp.action.DeleteActions; 076import org.nuxeo.ecm.webapp.base.InputController; 077import org.nuxeo.ecm.webapp.documentsLists.DocumentsListsManager; 078import org.nuxeo.ecm.webapp.helpers.EventManager; 079import org.nuxeo.ecm.webapp.helpers.EventNames; 080import org.nuxeo.runtime.api.Framework; 081 082/** 083 * Handles creation and edition of a document. 084 * 085 * @author <a href="mailto:rcaraghin@nuxeo.com">Razvan Caraghin</a> 086 * @author M.-A. Darche 087 */ 088@Name("documentActions") 089@Scope(CONVERSATION) 090public class DocumentActionsBean extends InputController implements DocumentActions, Serializable { 091 092 private static final long serialVersionUID = 1L; 093 094 private static final Log log = LogFactory.getLog(DocumentActionsBean.class); 095 096 public static final String LIFE_CYCLE_TRANSITION_KEY = "lifeCycleTransition"; 097 098 public static final String BLOB_ACTIONS_CATEGORY = "BLOB_ACTIONS"; 099 100 @In(create = true) 101 protected transient NavigationContext navigationContext; 102 103 @RequestParameter 104 protected String fileFieldFullName; 105 106 @RequestParameter 107 protected String filenameFieldFullName; 108 109 @RequestParameter 110 protected String filename; 111 112 @In(create = true, required = false) 113 protected transient CoreSession documentManager; 114 115 @In(required = false, create = true) 116 protected transient DocumentsListsManager documentsListsManager; 117 118 @In(create = true) 119 protected transient DeleteActions deleteActions; 120 121 @In(create = true, required = false) 122 protected transient ActionContextProvider actionContextProvider; 123 124 /** 125 * Boolean request parameter used to restore current tabs (current tab and subtab) after edition. 126 * <p> 127 * This is useful when editing the document from a layout toggled to edit mode from summary-like page. 128 * 129 * @since 5.6 130 */ 131 @RequestParameter 132 protected Boolean restoreCurrentTabs; 133 134 @In(create = true) 135 protected transient WebActions webActions; 136 137 protected String comment; 138 139 @In(create = true) 140 protected Map<String, String> messages; 141 142 @Override 143 @Factory(autoCreate = true, value = "currentDocumentType", scope = EVENT) 144 public Type getCurrentType() { 145 DocumentModel doc = navigationContext.getCurrentDocument(); 146 if (doc == null) { 147 return null; 148 } 149 return typeManager.getType(doc.getType()); 150 } 151 152 @Override 153 public Type getChangeableDocumentType() { 154 DocumentModel changeableDocument = navigationContext.getChangeableDocument(); 155 if (changeableDocument == null) { 156 // should we really do this ??? 157 navigationContext.setChangeableDocument(navigationContext.getCurrentDocument()); 158 changeableDocument = navigationContext.getChangeableDocument(); 159 } 160 if (changeableDocument == null) { 161 return null; 162 } 163 return typeManager.getType(changeableDocument.getType()); 164 } 165 166 public String getFileName(DocumentModel doc) { 167 String name = null; 168 if (filename != null && !"".equals(filename)) { 169 name = filename; 170 } else { 171 // try to fetch it from given field 172 if (filenameFieldFullName != null) { 173 String[] s = filenameFieldFullName.split(":"); 174 try { 175 name = (String) doc.getProperty(s[0], s[1]); 176 } catch (ArrayIndexOutOfBoundsException err) { 177 // ignore, filename is not really set 178 } 179 } 180 // try to fetch it from title 181 if (name == null || "".equals(name)) { 182 name = (String) doc.getProperty("dublincore", "title"); 183 } 184 } 185 return name; 186 } 187 188 @Override 189 public void download(DocumentView docView) { 190 if (docView == null) { 191 return; 192 } 193 DocumentLocation docLoc = docView.getDocumentLocation(); 194 // fix for NXP-1799 195 if (documentManager == null) { 196 RepositoryLocation loc = new RepositoryLocation(docLoc.getServerName()); 197 navigationContext.setCurrentServerLocation(loc); 198 documentManager = navigationContext.getOrCreateDocumentManager(); 199 } 200 DocumentModel doc = documentManager.getDocument(docLoc.getDocRef()); 201 if (doc == null) { 202 return; 203 } 204 String xpath = docView.getParameter(DocumentFileCodec.FILE_PROPERTY_PATH_KEY); 205 DownloadService downloadService = Framework.getService(DownloadService.class); 206 Blob blob = downloadService.resolveBlob(doc, xpath); 207 if (blob == null) { 208 log.warn("No blob for docView: " + docView); 209 return; 210 } 211 // get properties from document view 212 String filename = DocumentFileCodec.getFilename(doc, docView); 213 214 if (blob.getLength() > Functions.getBigFileSizeLimit()) { 215 FacesContext context = FacesContext.getCurrentInstance(); 216 String bigDownloadURL = BaseURL.getBaseURL() + "/" + downloadService.getDownloadUrl(doc, xpath, filename); 217 try { 218 context.getExternalContext().redirect(bigDownloadURL); 219 } catch (IOException e) { 220 log.error("Error while redirecting for big file downloader", e); 221 } 222 } else { 223 ComponentUtils.download(doc, xpath, blob, filename, "download"); 224 } 225 } 226 227 @Override 228 public String updateDocument(DocumentModel doc, Boolean restoreCurrentTabs) { 229 String tabId = null; 230 String subTabId = null; 231 boolean restoreTabs = Boolean.TRUE.equals(restoreCurrentTabs); 232 if (restoreTabs) { 233 // save current tabs 234 tabId = webActions.getCurrentTabId(); 235 subTabId = webActions.getCurrentSubTabId(); 236 } 237 Events.instance().raiseEvent(EventNames.BEFORE_DOCUMENT_CHANGED, doc); 238 try { 239 doc = documentManager.saveDocument(doc); 240 } catch (DocumentValidationException e) { 241 facesMessages.add(StatusMessage.Severity.ERROR, 242 messages.get("label.schema.constraint.violation.documentValidation"), e.getMessage()); 243 return null; 244 } 245 246 throwUpdateComments(doc); 247 documentManager.save(); 248 // some changes (versioning) happened server-side, fetch new one 249 navigationContext.invalidateCurrentDocument(); 250 facesMessages.add(StatusMessage.Severity.INFO, messages.get("document_modified"), messages.get(doc.getType())); 251 EventManager.raiseEventsOnDocumentChange(doc); 252 String res = navigationContext.navigateToDocument(doc, "after-edit"); 253 if (restoreTabs) { 254 // restore previously stored tabs; 255 webActions.setCurrentTabId(tabId); 256 webActions.setCurrentSubTabId(subTabId); 257 } 258 return res; 259 } 260 261 // kept for BBB 262 protected String updateDocument(DocumentModel doc) { 263 return updateDocument(doc, restoreCurrentTabs); 264 } 265 266 @Override 267 public String updateCurrentDocument() { 268 DocumentModel currentDocument = navigationContext.getCurrentDocument(); 269 return updateDocument(currentDocument); 270 } 271 272 @Override 273 public String createDocument() { 274 Type docType = typesTool.getSelectedType(); 275 return createDocument(docType.getId()); 276 } 277 278 @Override 279 public String createDocument(String typeName) { 280 Type docType = typeManager.getType(typeName); 281 // we cannot use typesTool as intermediary since the DataModel callback 282 // will alter whatever type we set 283 typesTool.setSelectedType(docType); 284 Map<String, Object> context = new HashMap<String, Object>(); 285 context.put(CoreEventConstants.PARENT_PATH, navigationContext.getCurrentDocument().getPathAsString()); 286 DocumentModel changeableDocument = documentManager.createDocumentModel(typeName, context); 287 navigationContext.setChangeableDocument(changeableDocument); 288 return navigationContext.getActionResult(changeableDocument, UserAction.CREATE); 289 } 290 291 @Override 292 public String saveDocument() { 293 DocumentModel changeableDocument = navigationContext.getChangeableDocument(); 294 return saveDocument(changeableDocument); 295 } 296 297 @RequestParameter 298 protected String parentDocumentPath; 299 300 @Override 301 public String saveDocument(DocumentModel newDocument) { 302 // Document has already been created if it has an id. 303 // This will avoid creation of many documents if user hit create button 304 // too many times. 305 if (newDocument.getId() != null) { 306 log.debug("Document " + newDocument.getName() + " already created"); 307 return navigationContext.navigateToDocument(newDocument, "after-create"); 308 } 309 PathSegmentService pss = Framework.getService(PathSegmentService.class); 310 DocumentModel currentDocument = navigationContext.getCurrentDocument(); 311 if (parentDocumentPath == null) { 312 if (currentDocument == null) { 313 // creating item at the root 314 parentDocumentPath = documentManager.getRootDocument().getPathAsString(); 315 } else { 316 parentDocumentPath = navigationContext.getCurrentDocument().getPathAsString(); 317 } 318 } 319 320 newDocument.setPathInfo(parentDocumentPath, pss.generatePathSegment(newDocument)); 321 322 try { 323 newDocument = documentManager.createDocument(newDocument); 324 } catch (DocumentValidationException e) { 325 facesMessages.add(StatusMessage.Severity.ERROR, 326 messages.get("label.schema.constraint.violation.documentValidation"), e.getMessage()); 327 return null; 328 } 329 documentManager.save(); 330 331 logDocumentWithTitle("Created the document: ", newDocument); 332 facesMessages.add(StatusMessage.Severity.INFO, messages.get("document_saved"), 333 messages.get(newDocument.getType())); 334 335 Events.instance().raiseEvent(EventNames.DOCUMENT_CHILDREN_CHANGED, currentDocument); 336 return navigationContext.navigateToDocument(newDocument, "after-create"); 337 } 338 339 @Override 340 public boolean getWriteRight() { 341 // TODO: WRITE is a high level compound permission (i.e. more like a 342 // user profile), public methods of the Nuxeo framework should only 343 // check atomic / specific permissions such as WRITE_PROPERTIES, 344 // REMOVE, ADD_CHILDREN depending on the action to execute instead 345 return documentManager.hasPermission(navigationContext.getCurrentDocument().getRef(), SecurityConstants.WRITE); 346 } 347 348 // Send the comment of the update to the Core 349 private void throwUpdateComments(DocumentModel changeableDocument) { 350 if (comment != null && !"".equals(comment)) { 351 changeableDocument.putContextData("comment", comment); 352 } 353 } 354 355 @Override 356 public boolean getCanUnpublish() { 357 List<DocumentModel> docList = documentsListsManager.getWorkingList(DocumentsListsManager.CURRENT_DOCUMENT_SECTION_SELECTION); 358 359 if (!(docList == null || docList.isEmpty()) && deleteActions.checkDeletePermOnParents(docList)) { 360 for (DocumentModel document : docList) { 361 if (document.hasFacet(FacetNames.PUBLISH_SPACE) || document.hasFacet(FacetNames.MASTER_PUBLISH_SPACE)) { 362 return false; 363 } 364 } 365 return true; 366 } 367 return false; 368 } 369 370 @Override 371 @Observer(EventNames.BEFORE_DOCUMENT_CHANGED) 372 public void followTransition(DocumentModel changedDocument) { 373 String transitionToFollow = (String) changedDocument.getContextData(LIFE_CYCLE_TRANSITION_KEY); 374 if (transitionToFollow != null) { 375 documentManager.followTransition(changedDocument.getRef(), transitionToFollow); 376 documentManager.save(); 377 } 378 } 379 380 /** 381 * @since 7.3 382 */ 383 public List<Action> getBlobActions(DocumentModel doc, String blobXPath, Blob blob) { 384 ActionContext ctx = actionContextProvider.createActionContext(); 385 ctx.putLocalVariable("document", doc); 386 ctx.putLocalVariable("blob", blob); 387 ctx.putLocalVariable("blobXPath", blobXPath); 388 return webActions.getActionsList(BLOB_ACTIONS_CATEGORY, ctx, true); 389 } 390 391 /** 392 * @since 7.3 393 */ 394 @WebRemote 395 public List<AppLink> getAppLinks(String docId, String blobXPath) { 396 DocumentRef docRef = new IdRef(docId); 397 DocumentModel doc = documentManager.getDocument(docRef); 398 Serializable value = doc.getPropertyValue(blobXPath); 399 400 if (value == null || !(value instanceof ManagedBlob)) { 401 return null; 402 } 403 ManagedBlob managedBlob = (ManagedBlob) value; 404 405 BlobManager blobManager = Framework.getService(BlobManager.class); 406 BlobProvider blobProvider = blobManager.getBlobProvider(managedBlob.getProviderId()); 407 if (blobProvider == null) { 408 log.error("No registered blob provider for key: " + managedBlob.getKey()); 409 return null; 410 } 411 412 String user = documentManager.getPrincipal().getName(); 413 414 try { 415 return blobProvider.getAppLinks(user, managedBlob); 416 } catch (IOException e) { 417 log.error("Failed to retrieve application links", e); 418 } 419 return null; 420 } 421 422 /** 423 * Checks if the main blob can be updated by a user-initiated action. 424 * 425 * @since 7.10 426 */ 427 public boolean getCanUpdateMainBlob() { 428 DocumentModel doc = navigationContext.getCurrentDocument(); 429 if (doc == null) { 430 return false; 431 } 432 BlobHolder blobHolder = doc.getAdapter(BlobHolder.class); 433 if (blobHolder == null) { 434 return false; 435 } 436 Blob blob = blobHolder.getBlob(); 437 if (blob == null) { 438 return true; 439 } 440 BlobProvider blobProvider = Framework.getService(BlobManager.class).getBlobProvider(blob); 441 if (blobProvider == null) { 442 return true; 443 } 444 return blobProvider.supportsUserUpdate(); 445 } 446 447}