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