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