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}