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