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