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}