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