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}