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