001/*
002 * (C) Copyright 2006-2016 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 * $Id$
020 */
021package org.nuxeo.ecm.webapp.clipboard;
022
023import static org.jboss.seam.ScopeType.EVENT;
024import static org.jboss.seam.ScopeType.SESSION;
025
026import java.io.File;
027import java.io.IOException;
028import java.io.Serializable;
029import java.util.ArrayList;
030import java.util.Collections;
031import java.util.HashMap;
032import java.util.HashSet;
033import java.util.LinkedList;
034import java.util.List;
035import java.util.Map;
036import java.util.Set;
037
038import javax.faces.context.FacesContext;
039import javax.servlet.http.HttpServletRequest;
040
041import org.apache.commons.logging.Log;
042import org.apache.commons.logging.LogFactory;
043import org.jboss.seam.annotations.Factory;
044import org.jboss.seam.annotations.In;
045import org.jboss.seam.annotations.Name;
046import org.jboss.seam.annotations.Scope;
047import org.jboss.seam.annotations.remoting.WebRemote;
048import org.jboss.seam.annotations.web.RequestParameter;
049import org.jboss.seam.core.Events;
050import org.jboss.seam.faces.FacesMessages;
051import org.jboss.seam.international.LocaleSelector;
052import org.jboss.seam.international.StatusMessage;
053import org.nuxeo.ecm.core.api.CoreSession;
054import org.nuxeo.ecm.core.api.CoreSession.CopyOption;
055import org.nuxeo.ecm.core.api.DocumentModel;
056import org.nuxeo.ecm.core.api.DocumentModelList;
057import org.nuxeo.ecm.core.api.DocumentRef;
058import org.nuxeo.ecm.core.api.IdRef;
059import org.nuxeo.ecm.core.api.LifeCycleConstants;
060import org.nuxeo.ecm.core.api.NuxeoException;
061import org.nuxeo.ecm.core.api.security.SecurityConstants;
062import org.nuxeo.ecm.core.io.download.DownloadService;
063import org.nuxeo.ecm.core.schema.FacetNames;
064import org.nuxeo.ecm.core.schema.SchemaManager;
065import org.nuxeo.ecm.platform.actions.Action;
066import org.nuxeo.ecm.platform.types.TypeManager;
067import org.nuxeo.ecm.platform.ui.web.api.NavigationContext;
068import org.nuxeo.ecm.platform.ui.web.api.WebActions;
069import org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants;
070import org.nuxeo.ecm.platform.ui.web.cache.SeamCacheHelper;
071import org.nuxeo.ecm.platform.ui.web.tag.fn.Functions;
072import org.nuxeo.ecm.platform.ui.web.util.BaseURL;
073import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils;
074import org.nuxeo.ecm.webapp.documentsLists.DocumentsListDescriptor;
075import org.nuxeo.ecm.webapp.documentsLists.DocumentsListsManager;
076import org.nuxeo.ecm.webapp.helpers.EventManager;
077import org.nuxeo.ecm.webapp.helpers.EventNames;
078import org.nuxeo.runtime.api.Framework;
079
080/**
081 * This is the action listener behind the copy/paste template that knows how to copy/paste the selected user data to the
082 * target action listener, and also create/remove the corresponding objects into the backend.
083 *
084 * @author <a href="mailto:rcaraghin@nuxeo.com">Razvan Caraghin</a>
085 */
086@Name("clipboardActions")
087@Scope(SESSION)
088public class ClipboardActionsBean implements ClipboardActions, Serializable {
089
090    private static final long serialVersionUID = -2407222456116573225L;
091
092    private static final Log log = LogFactory.getLog(ClipboardActionsBean.class);
093
094    @In(create = true, required = false)
095    protected FacesMessages facesMessages;
096
097    @In(create = true)
098    protected Map<String, String> messages;
099
100    @In(create = true, required = false)
101    protected transient CoreSession documentManager;
102
103    @In(create = true)
104    protected transient DocumentsListsManager documentsListsManager;
105
106    @In(create = true)
107    protected TypeManager typeManager;
108
109    @In(create = true)
110    protected NavigationContext navigationContext;
111
112    @In(create = true)
113    protected transient WebActions webActions; // it is serializable
114
115    @In(create = true)
116    protected transient LocaleSelector localeSelector;
117
118    @RequestParameter()
119    protected String workListDocId;
120
121    private String currentSelectedList;
122
123    private String previouslySelectedList;
124
125    private transient List<String> availableLists;
126
127    private transient List<DocumentsListDescriptor> descriptorsForAvailableLists;
128
129    private Boolean canEditSelectedDocs;
130
131    private transient Map<String, List<Action>> actionCache;
132
133    public void releaseClipboardableDocuments() {
134    }
135
136    public boolean isInitialized() {
137        return documentManager != null;
138    }
139
140    public void putSelectionInWorkList(Boolean forceAppend) {
141        canEditSelectedDocs = null;
142        if (!documentsListsManager.isWorkingListEmpty(DocumentsListsManager.CURRENT_DOCUMENT_SELECTION)) {
143            putSelectionInWorkList(
144                    documentsListsManager.getWorkingList(DocumentsListsManager.CURRENT_DOCUMENT_SELECTION), forceAppend);
145            autoSelectCurrentList(DocumentsListsManager.DEFAULT_WORKING_LIST);
146        } else {
147            log.debug("No selectable Documents in context to process copy on...");
148        }
149        log.debug("add to worklist processed...");
150    }
151
152    public void putSelectionInWorkList() {
153        putSelectionInWorkList(false);
154    }
155
156    public void putSelectionInDefaultWorkList() {
157        canEditSelectedDocs = null;
158        if (!documentsListsManager.isWorkingListEmpty(DocumentsListsManager.CURRENT_DOCUMENT_SELECTION)) {
159            List<DocumentModel> docsList = documentsListsManager.getWorkingList(DocumentsListsManager.CURRENT_DOCUMENT_SELECTION);
160            Object[] params = { docsList.size() };
161            facesMessages.add(StatusMessage.Severity.INFO, "#0 " + messages.get("n_copied_docs"), params);
162            documentsListsManager.addToWorkingList(DocumentsListsManager.DEFAULT_WORKING_LIST, docsList);
163
164            // auto select clipboard
165            autoSelectCurrentList(DocumentsListsManager.DEFAULT_WORKING_LIST);
166
167        } else {
168            log.debug("No selectable Documents in context to process copy on...");
169        }
170        log.debug("add to worklist processed...");
171    }
172
173    @WebRemote
174    public void putInClipboard(String docId) {
175        DocumentModel doc = documentManager.getDocument(new IdRef(docId));
176        documentsListsManager.addToWorkingList(DocumentsListsManager.CLIPBOARD, doc);
177        Object[] params = { 1 };
178        facesMessages.add(StatusMessage.Severity.INFO, "#0 " + messages.get("n_copied_docs"), params);
179
180        autoSelectCurrentList(DocumentsListsManager.CLIPBOARD);
181    }
182
183    public void putSelectionInClipboard() {
184        canEditSelectedDocs = null;
185        if (!documentsListsManager.isWorkingListEmpty(DocumentsListsManager.CURRENT_DOCUMENT_SELECTION)) {
186            List<DocumentModel> docsList = documentsListsManager.getWorkingList(DocumentsListsManager.CURRENT_DOCUMENT_SELECTION);
187            Object[] params = { docsList.size() };
188            facesMessages.add(StatusMessage.Severity.INFO, "#0 " + messages.get("n_copied_docs"), params);
189
190            documentsListsManager.addToWorkingList(DocumentsListsManager.CLIPBOARD, docsList);
191
192            // auto select clipboard
193            autoSelectCurrentList(DocumentsListsManager.CLIPBOARD);
194
195        } else {
196            log.debug("No selectable Documents in context to process copy on...");
197        }
198        log.debug("add to worklist processed...");
199    }
200
201    public void putSelectionInWorkList(List<DocumentModel> docsList) {
202        putSelectionInWorkList(docsList, false);
203    }
204
205    public void putSelectionInWorkList(List<DocumentModel> docsList, Boolean forceAppend) {
206        canEditSelectedDocs = null;
207        if (null != docsList) {
208            Object[] params = { docsList.size() };
209            facesMessages.add(StatusMessage.Severity.INFO, "#0 " + messages.get("n_added_to_worklist_docs"), params);
210
211            // Add to the default working list
212            documentsListsManager.addToWorkingList(getCurrentSelectedListName(), docsList, forceAppend);
213            log.debug("Elements copied to clipboard...");
214
215        } else {
216            log.debug("No copiedDocs to process copy on...");
217        }
218
219        log.debug("add to worklist processed...");
220    }
221
222    @Deprecated
223    public void copySelection(List<DocumentModel> copiedDocs) {
224        if (null != copiedDocs) {
225            Object[] params = { copiedDocs.size() };
226            facesMessages.add(StatusMessage.Severity.INFO, "#0 " + messages.get("n_copied_docs"), params);
227
228            // clipboard.copy(copiedDocs);
229
230            // Reset + Add to clipboard list
231            documentsListsManager.resetWorkingList(DocumentsListsManager.CLIPBOARD);
232            documentsListsManager.addToWorkingList(DocumentsListsManager.CLIPBOARD, copiedDocs);
233
234            // Add to the default working list
235            documentsListsManager.addToWorkingList(copiedDocs);
236            log.debug("Elements copied to clipboard...");
237
238        } else {
239            log.debug("No copiedDocs to process copy on...");
240        }
241
242        log.debug("Copy processed...");
243    }
244
245    public boolean exists(DocumentRef ref) {
246        return ref != null && documentManager.exists(ref);
247    }
248
249    public String removeWorkListItem(DocumentRef ref) {
250        DocumentModel doc = null;
251        if (exists(ref)) {
252            doc = documentManager.getDocument(ref);
253        } else { // document was permanently deleted so let's use the one in the work list
254            List<DocumentModel> workingListDocs = documentsListsManager.getWorkingList(getCurrentSelectedListName());
255            for (DocumentModel wDoc : workingListDocs) {
256                if (wDoc.getRef().equals(ref)) {
257                    doc = wDoc;
258                }
259            }
260        }
261        documentsListsManager.removeFromWorkingList(getCurrentSelectedListName(), doc);
262        return null;
263    }
264
265    public String clearWorkingList() {
266        documentsListsManager.resetWorkingList(getCurrentSelectedListName());
267        return null;
268    }
269
270    public String pasteDocumentList(String listName) {
271        return pasteDocumentList(documentsListsManager.getWorkingList(listName));
272    }
273
274    public String pasteDocumentListInside(String listName, String docId) {
275        return pasteDocumentListInside(documentsListsManager.getWorkingList(listName), docId);
276    }
277
278    public String pasteDocumentList(List<DocumentModel> docPaste) {
279        DocumentModel currentDocument = navigationContext.getCurrentDocument();
280        if (null != docPaste) {
281            List<DocumentModel> newDocs = recreateDocumentsWithNewParent(getParent(currentDocument), docPaste);
282
283            Object[] params = { newDocs.size() };
284            facesMessages.add(StatusMessage.Severity.INFO, "#0 " + messages.get("n_pasted_docs"), params);
285
286            EventManager.raiseEventsOnDocumentSelected(currentDocument);
287            Events.instance().raiseEvent(EventNames.DOCUMENT_CHILDREN_CHANGED, currentDocument);
288
289            log.debug("Elements pasted and created into the backend...");
290        } else {
291            log.debug("No docPaste to process paste on...");
292        }
293
294        return null;
295    }
296
297    public String pasteDocumentListInside(List<DocumentModel> docPaste, String docId) {
298        DocumentModel targetDoc = documentManager.getDocument(new IdRef(docId));
299        if (null != docPaste) {
300            List<DocumentModel> newDocs = recreateDocumentsWithNewParent(targetDoc, docPaste);
301
302            Object[] params = { newDocs.size() };
303            facesMessages.add(StatusMessage.Severity.INFO, "#0 " + messages.get("n_pasted_docs"), params);
304
305            EventManager.raiseEventsOnDocumentSelected(targetDoc);
306            Events.instance().raiseEvent(EventNames.DOCUMENT_CHILDREN_CHANGED, targetDoc);
307
308            log.debug("Elements pasted and created into the backend...");
309        } else {
310            log.debug("No docPaste to process paste on...");
311        }
312
313        return null;
314    }
315
316    public List<DocumentModel> moveDocumentsToNewParent(DocumentModel destFolder, List<DocumentModel> docs) {
317        DocumentRef destFolderRef = destFolder.getRef();
318        boolean destinationIsDeleted = LifeCycleConstants.DELETED_STATE.equals(destFolder.getCurrentLifeCycleState());
319        List<DocumentModel> newDocs = new ArrayList<>();
320        StringBuilder sb = new StringBuilder();
321        for (DocumentModel docModel : docs) {
322            DocumentRef sourceFolderRef = docModel.getParentRef();
323
324            String sourceType = docModel.getType();
325            boolean canRemoveDoc = documentManager.hasPermission(sourceFolderRef, SecurityConstants.REMOVE_CHILDREN);
326            boolean canPasteInCurrentFolder = typeManager.isAllowedSubType(sourceType, destFolder.getType(),
327                    navigationContext.getCurrentDocument());
328            boolean sameFolder = sourceFolderRef.equals(destFolderRef);
329            if (canRemoveDoc && canPasteInCurrentFolder && !sameFolder) {
330                if (destinationIsDeleted) {
331                    if (checkDeletedState(docModel)) {
332                        DocumentModel newDoc = documentManager.move(docModel.getRef(), destFolderRef, null);
333                        setDeleteState(newDoc);
334                        newDocs.add(newDoc);
335                    } else {
336                        addWarnMessage(sb, docModel);
337                    }
338                } else {
339                    DocumentModel newDoc = documentManager.move(docModel.getRef(), destFolderRef, null);
340                    newDocs.add(newDoc);
341                }
342            }
343        }
344        documentManager.save();
345
346        if (sb.length() > 0) {
347            facesMessages.add(StatusMessage.Severity.WARN, sb.toString());
348        }
349        return newDocs;
350    }
351
352    public String moveDocumentList(String listName, String docId) {
353        List<DocumentModel> docs = documentsListsManager.getWorkingList(listName);
354        DocumentModel targetDoc = documentManager.getDocument(new IdRef(docId));
355        // Get all parent folders
356        Set<DocumentRef> parentRefs = new HashSet<>();
357        for (DocumentModel doc : docs) {
358            parentRefs.add(doc.getParentRef());
359        }
360
361        List<DocumentModel> newDocs = moveDocumentsToNewParent(targetDoc, docs);
362
363        documentsListsManager.resetWorkingList(listName);
364
365        Object[] params = { newDocs.size() };
366        facesMessages.add(StatusMessage.Severity.INFO, "#0 " + messages.get("n_moved_docs"), params);
367
368        EventManager.raiseEventsOnDocumentSelected(targetDoc);
369        Events.instance().raiseEvent(EventNames.DOCUMENT_CHILDREN_CHANGED, targetDoc);
370
371        // Send event to all initial parents
372        for (DocumentRef docRef : parentRefs) {
373            Events.instance().raiseEvent(EventNames.DOCUMENT_CHILDREN_CHANGED, documentManager.getDocument(docRef));
374        }
375
376        log.debug("Elements moved and created into the backend...");
377
378        return null;
379    }
380
381    public String moveDocumentList(String listName) {
382        DocumentModel currentDocument = navigationContext.getCurrentDocument();
383        return moveDocumentList(listName, currentDocument.getId());
384    }
385
386    public String moveWorkingList() {
387        try {
388            moveDocumentList(getCurrentSelectedListName());
389        } catch (NuxeoException e) {
390            log.error("moveWorkingList failed" + e.getMessage(), e);
391            facesMessages.add(StatusMessage.Severity.WARN, messages.get("invalid_operation"));
392        }
393        return null;
394    }
395
396    public String pasteWorkingList() {
397        try {
398            pasteDocumentList(getCurrentSelectedList());
399        } catch (NuxeoException e) {
400            log.error("pasteWorkingList failed" + e.getMessage(), e);
401            facesMessages.add(StatusMessage.Severity.WARN, messages.get("invalid_operation"));
402        }
403        return null;
404    }
405
406    public String pasteClipboard() {
407        try {
408            pasteDocumentList(DocumentsListsManager.CLIPBOARD);
409            returnToPreviouslySelectedList();
410        } catch (NuxeoException e) {
411            log.error("pasteClipboard failed" + e.getMessage(), e);
412            facesMessages.add(StatusMessage.Severity.WARN, messages.get("invalid_operation"));
413
414        }
415        return null;
416    }
417
418    @WebRemote
419    public String pasteClipboardInside(String docId) {
420        pasteDocumentListInside(DocumentsListsManager.CLIPBOARD, docId);
421        return null;
422    }
423
424    @WebRemote
425    public String moveClipboardInside(String docId) {
426        moveDocumentList(DocumentsListsManager.CLIPBOARD, docId);
427        return null;
428    }
429
430    /**
431     * Creates the documents in the backend under the target parent.
432     */
433    protected List<DocumentModel> recreateDocumentsWithNewParent(DocumentModel parent, List<DocumentModel> documents) {
434
435        List<DocumentModel> newDocuments = new ArrayList<>();
436
437        if (null == parent || null == documents) {
438            log.error("Null params received, returning...");
439            return newDocuments;
440        }
441
442        List<DocumentModel> documentsToPast = new LinkedList<>();
443
444        // filter list on content type
445        for (DocumentModel doc : documents) {
446            if (typeManager.isAllowedSubType(doc.getType(), parent.getType(), navigationContext.getCurrentDocument())) {
447                documentsToPast.add(doc);
448            }
449        }
450
451        // copying proxy or document
452        boolean isPublishSpace = isPublishSpace(parent);
453        boolean destinationIsDeleted = LifeCycleConstants.DELETED_STATE.equals(parent.getCurrentLifeCycleState());
454        List<DocumentRef> docRefs = new ArrayList<>();
455        List<DocumentRef> proxyRefs = new ArrayList<>();
456        StringBuilder sb = new StringBuilder();
457        for (DocumentModel doc : documentsToPast) {
458            if (destinationIsDeleted && !checkDeletedState(doc)) {
459                addWarnMessage(sb, doc);
460            } else if (doc.isProxy() && !isPublishSpace) {
461                // in a non-publish space, we want to expand proxies into
462                // normal docs
463                proxyRefs.add(doc.getRef());
464            } else {
465                // copy as is
466                docRefs.add(doc.getRef());
467            }
468        }
469        if (!proxyRefs.isEmpty()) {
470            newDocuments.addAll(documentManager.copyProxyAsDocument(proxyRefs, parent.getRef(),
471                    CopyOption.RESET_LIFE_CYCLE));
472        }
473        if (!docRefs.isEmpty()) {
474            newDocuments.addAll(documentManager.copy(docRefs, parent.getRef(), CopyOption.RESET_LIFE_CYCLE));
475        }
476        if (destinationIsDeleted) {
477            for (DocumentModel d : newDocuments) {
478                setDeleteState(d);
479            }
480        }
481        documentManager.save();
482        if (sb.length() > 0) {
483            facesMessages.add(StatusMessage.Severity.WARN, sb.toString());
484        }
485        return newDocuments;
486    }
487
488    protected boolean checkDeletedState(DocumentModel doc) {
489        if (LifeCycleConstants.DELETED_STATE.equals(doc.getCurrentLifeCycleState())) {
490            return true;
491        }
492        if (doc.getAllowedStateTransitions().contains(LifeCycleConstants.DELETE_TRANSITION)) {
493            return true;
494        }
495        return false;
496    }
497
498    protected void setDeleteState(DocumentModel doc) {
499        if (doc.getAllowedStateTransitions().contains(LifeCycleConstants.DELETE_TRANSITION)) {
500            doc.followTransition(LifeCycleConstants.DELETE_TRANSITION);
501        }
502    }
503
504    protected void addWarnMessage(StringBuilder sb, DocumentModel doc) {
505        if (sb.length() == 0) {
506            sb.append(messages.get("document_no_deleted_state"));
507            sb.append("'").append(doc.getTitle()).append("'");
508        } else {
509            sb.append(", '").append(doc.getTitle()).append("'");
510        }
511    }
512
513    /**
514     * Check if the container is a publish space. If this is not the case, a proxy copied to it will be recreated as a
515     * new document.
516     */
517    protected boolean isPublishSpace(DocumentModel container) {
518        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
519        Set<String> publishSpaces = schemaManager.getDocumentTypeNamesForFacet(FacetNames.PUBLISH_SPACE);
520        if (publishSpaces == null || publishSpaces.isEmpty()) {
521            publishSpaces = new HashSet<>();
522        }
523        return publishSpaces.contains(container.getType());
524    }
525
526    /**
527     * Gets the parent document under the paste should be performed.
528     * <p>
529     * Rules:
530     * <p>
531     * In general the currentDocument is the parent. Exceptions to this rule: when the currentDocument is a domain or
532     * null. If Domain then content root is the parent. If null is passed, then the JCR root is taken as parent.
533     */
534    protected DocumentModel getParent(DocumentModel currentDocument) {
535
536        if (currentDocument.isFolder()) {
537            return currentDocument;
538        }
539
540        DocumentModelList parents = navigationContext.getCurrentPath();
541        for (int i = parents.size() - 1; i >= 0; i--) {
542            DocumentModel parent = parents.get(i);
543            if (parent.isFolder()) {
544                return parent;
545            }
546        }
547
548        return null;
549    }
550
551    @Factory(value = "isCurrentWorkListEmpty", scope = EVENT)
552    public boolean factoryForIsCurrentWorkListEmpty() {
553        return isWorkListEmpty();
554    }
555
556    public boolean isWorkListEmpty() {
557        return documentsListsManager.isWorkingListEmpty(getCurrentSelectedListName());
558    }
559
560    public String exportWorklistAsZip() {
561        return exportWorklistAsZip(documentsListsManager.getWorkingList(getCurrentSelectedListName()));
562    }
563
564    public String exportAllBlobsFromWorkingListAsZip() {
565        return exportWorklistAsZip();
566    }
567
568    public String exportMainBlobFromWorkingListAsZip() {
569        return exportWorklistAsZip();
570    }
571
572    public String exportWorklistAsZip(List<DocumentModel> documents) {
573        return exportWorklistAsZip(documents, true);
574    }
575
576    public String exportWorklistAsZip(DocumentModel document) {
577        return exportWorklistAsZip(Collections.singletonList(document), true);
578    }
579
580    /**
581     * Checks if copy action is available in the context of the current Document.
582     * <p>
583     * Condition: the list of selected documents is not empty.
584     */
585    public boolean getCanCopy() {
586        if (navigationContext.getCurrentDocument() == null) {
587            return false;
588        }
589        return !documentsListsManager.isWorkingListEmpty(DocumentsListsManager.CURRENT_DOCUMENT_SELECTION);
590    }
591
592    /**
593     * Checks if the Paste action is available in the context of the current Document. Conditions:
594     * <p>
595     * <ul>
596     * <li>list is not empty
597     * <li>user has the needed permissions on the current document
598     * <li>the content of the list can be added as children of the current document
599     * </ul>
600     */
601    public boolean getCanPaste(String listName) {
602
603        DocumentModel currentDocument = navigationContext.getCurrentDocument();
604
605        if (documentsListsManager.isWorkingListEmpty(listName) || currentDocument == null) {
606            return false;
607        }
608
609        DocumentModel pasteTarget = getParent(navigationContext.getCurrentDocument());
610        if (pasteTarget == null) {
611            // parent may be unreachable (right inheritance blocked)
612            return false;
613        }
614        if (!documentManager.hasPermission(pasteTarget.getRef(), SecurityConstants.ADD_CHILDREN)) {
615            return false;
616        } else {
617            // filter on allowed content types
618            // see if at least one doc can be pasted
619            // String pasteTypeName = clipboard.getClipboardDocumentType();
620            List<String> pasteTypesName = documentsListsManager.getWorkingListTypes(listName);
621            for (String pasteTypeName : pasteTypesName) {
622                if (typeManager.isAllowedSubType(pasteTypeName, pasteTarget.getType(),
623                        navigationContext.getCurrentDocument())) {
624                    return true;
625                }
626            }
627            return false;
628        }
629    }
630
631    public boolean getCanPasteInside(String listName, DocumentModel document) {
632        if (documentsListsManager.isWorkingListEmpty(listName) || document == null) {
633            return false;
634        }
635
636        if (!documentManager.hasPermission(document.getRef(), SecurityConstants.ADD_CHILDREN)) {
637            return false;
638        } else {
639            // filter on allowed content types
640            // see if at least one doc can be pasted
641            // String pasteTypeName = clipboard.getClipboardDocumentType();
642            List<String> pasteTypesName = documentsListsManager.getWorkingListTypes(listName);
643            for (String pasteTypeName : pasteTypesName) {
644                if (typeManager.isAllowedSubType(pasteTypeName, document.getType(),
645                        navigationContext.getCurrentDocument())) {
646                    return true;
647                }
648            }
649            return false;
650        }
651    }
652
653    /**
654     * Checks if the Move action is available in the context of the document document. Conditions:
655     * <p>
656     * <ul>
657     * <li>list is not empty
658     * <li>user has the needed permissions on the document
659     * <li>an element in the list can be removed from its folder and added as child of the current document
660     * </ul>
661     */
662    public boolean getCanMoveInside(String listName, DocumentModel document) {
663        if (documentsListsManager.isWorkingListEmpty(listName) || document == null) {
664            return false;
665        }
666        DocumentRef destFolderRef = document.getRef();
667        DocumentModel destFolder = document;
668        if (!documentManager.hasPermission(destFolderRef, SecurityConstants.ADD_CHILDREN)) {
669            return false;
670        } else {
671            // filter on allowed content types
672            // see if at least one doc can be removed and pasted
673            for (DocumentModel docModel : documentsListsManager.getWorkingList(listName)) {
674                // skip deleted documents
675                if (!exists(docModel.getRef())) {
676                    continue;
677                }
678                DocumentRef sourceFolderRef = docModel.getParentRef();
679                String sourceType = docModel.getType();
680                boolean canRemoveDoc = documentManager.hasPermission(sourceFolderRef, SecurityConstants.REMOVE_CHILDREN);
681                boolean canPasteInCurrentFolder = typeManager.isAllowedSubType(sourceType, destFolder.getType(),
682                        navigationContext.getCurrentDocument());
683                boolean sameFolder = sourceFolderRef.equals(destFolderRef);
684                if (canRemoveDoc && canPasteInCurrentFolder && !sameFolder) {
685                    return true;
686                }
687            }
688            return false;
689        }
690    }
691
692    /**
693     * Checks if the Move action is available in the context of the current Document. Conditions:
694     * <p>
695     * <ul>
696     * <li>list is not empty
697     * <li>user has the needed permissions on the current document
698     * <li>an element in the list can be removed from its folder and added as child of the current document
699     * </ul>
700     */
701    public boolean getCanMove(String listName) {
702        DocumentModel currentDocument = navigationContext.getCurrentDocument();
703        return getCanMoveInside(listName, currentDocument);
704    }
705
706    public boolean getCanPasteWorkList() {
707        return getCanPaste(getCurrentSelectedListName());
708    }
709
710    public boolean getCanMoveWorkingList() {
711        return getCanMove(getCurrentSelectedListName());
712    }
713
714    public boolean getCanPasteFromClipboard() {
715        return getCanPaste(DocumentsListsManager.CLIPBOARD);
716    }
717
718    public boolean getCanPasteFromClipboardInside(DocumentModel document) {
719        return getCanPasteInside(DocumentsListsManager.CLIPBOARD, document);
720    }
721
722    public boolean getCanMoveFromClipboardInside(DocumentModel document) {
723        return getCanMoveInside(DocumentsListsManager.CLIPBOARD, document);
724    }
725
726    public void setCurrentSelectedList(String listId) {
727        if (listId != null && !listId.equals(currentSelectedList)) {
728            currentSelectedList = listId;
729            canEditSelectedDocs = null;
730        }
731    }
732
733    @RequestParameter()
734    String listIdToSelect;
735
736    public void selectList() {
737        if (listIdToSelect != null) {
738            setCurrentSelectedList(listIdToSelect);
739        }
740    }
741
742    public List<DocumentModel> getCurrentSelectedList() {
743        return documentsListsManager.getWorkingList(getCurrentSelectedListName());
744    }
745
746    public String getCurrentSelectedListName() {
747        if (currentSelectedList == null) {
748            if (!getAvailableLists().isEmpty()) {
749                setCurrentSelectedList(availableLists.get(0));
750            }
751        }
752        return currentSelectedList;
753    }
754
755    public String getCurrentSelectedListTitle() {
756        String title = null;
757        String listName = getCurrentSelectedListName();
758        if (listName != null) {
759            DocumentsListDescriptor desc = documentsListsManager.getWorkingListDescriptor(listName);
760            if (desc != null) {
761                title = desc.getTitle();
762            }
763        }
764        return title;
765    }
766
767    public List<String> getAvailableLists() {
768        if (availableLists == null) {
769            availableLists = documentsListsManager.getWorkingListNamesForCategory("CLIPBOARD");
770        }
771        return availableLists;
772    }
773
774    public List<DocumentsListDescriptor> getDescriptorsForAvailableLists() {
775        if (descriptorsForAvailableLists == null) {
776            List<String> availableLists = getAvailableLists();
777            descriptorsForAvailableLists = new ArrayList<>();
778            for (String lName : availableLists) {
779                descriptorsForAvailableLists.add(documentsListsManager.getWorkingListDescriptor(lName));
780            }
781        }
782        return descriptorsForAvailableLists;
783    }
784
785    public List<Action> getActionsForCurrentList() {
786        String lstName = getCurrentSelectedListName();
787        if (isWorkListEmpty()) {
788            // we use cache here since this is a very common case ...
789            if (actionCache == null) {
790                actionCache = new HashMap<>();
791            }
792            if (!actionCache.containsKey(lstName)) {
793                actionCache.put(lstName, webActions.getActionsList(lstName + "_LIST"));
794            }
795            return actionCache.get(lstName);
796        } else {
797            return webActions.getActionsList(lstName + "_LIST");
798        }
799    }
800
801    public List<Action> getActionsForSelection() {
802        return webActions.getActionsList(DocumentsListsManager.CURRENT_DOCUMENT_SELECTION + "_LIST", false);
803    }
804
805    private void autoSelectCurrentList(String listName) {
806        previouslySelectedList = getCurrentSelectedListName();
807        setCurrentSelectedList(listName);
808    }
809
810    private void returnToPreviouslySelectedList() {
811        setCurrentSelectedList(previouslySelectedList);
812    }
813
814    public boolean getCanEditSelectedDocs() {
815        if (canEditSelectedDocs == null) {
816            if (getCurrentSelectedList().isEmpty()) {
817                canEditSelectedDocs = false;
818            } else {
819                final List<DocumentModel> selectedDocs = getCurrentSelectedList();
820
821                // check selected docs
822                canEditSelectedDocs = checkWritePerm(selectedDocs);
823            }
824        }
825        return canEditSelectedDocs;
826    }
827
828    @Deprecated
829    // no longer used by the user_clipboard.xhtml template
830    public boolean getCanEditListDocs(String listName) {
831        final List<DocumentModel> docs = documentsListsManager.getWorkingList(listName);
832
833        final boolean canEdit;
834        if (docs.isEmpty()) {
835            canEdit = false;
836        } else {
837            // check selected docs
838            canEdit = checkWritePerm(docs);
839        }
840        return canEdit;
841    }
842
843    private boolean checkWritePerm(List<DocumentModel> selectedDocs) {
844        for (DocumentModel documentModel : selectedDocs) {
845            boolean canWrite = documentManager.hasPermission(documentModel.getRef(), SecurityConstants.WRITE_PROPERTIES);
846            if (!canWrite) {
847                return false;
848            }
849        }
850        return true;
851    }
852
853    public boolean isCacheEnabled() {
854        if (!SeamCacheHelper.canUseSeamCache()) {
855            return false;
856        }
857        return isWorkListEmpty();
858    }
859
860    public String getCacheKey() {
861        return getCurrentSelectedListName() + "::" + localeSelector.getLocaleString();
862    }
863
864    public boolean isCacheEnabledForSelection() {
865        if (!SeamCacheHelper.canUseSeamCache()) {
866            return false;
867        }
868        return documentsListsManager.isWorkingListEmpty(DocumentsListsManager.CURRENT_DOCUMENT_SELECTION);
869    }
870
871    @Override
872    public String exportWorklistAsZip(List<DocumentModel> documents, boolean exportAllBlobs) {
873        try {
874            FacesContext context = FacesContext.getCurrentInstance();
875            DocumentListZipExporter zipExporter = new DocumentListZipExporter();
876            File tmpFile = zipExporter.exportWorklistAsZip(documents, documentManager, exportAllBlobs);
877            if (tmpFile == null) {
878                // empty zip file, do nothing
879                facesMessages.add(StatusMessage.Severity.INFO, messages.get("label.clipboard.emptyDocuments"));
880                return null;
881            } else {
882                if (tmpFile.length() > Functions.getBigFileSizeLimit()) {
883                    HttpServletRequest request = (HttpServletRequest) context.getExternalContext().getRequest();
884                    request.setAttribute(NXAuthConstants.DISABLE_REDIRECT_REQUEST_KEY, true);
885                    String zipDownloadURL = BaseURL.getBaseURL(request);
886                    zipDownloadURL += DownloadService.NXBIGZIPFILE + "/";
887                    zipDownloadURL += tmpFile.getName();
888                    try {
889                        context.getExternalContext().redirect(zipDownloadURL);
890                    } catch (IOException e) {
891                        log.error("Error while redirecting for big file downloader", e);
892                    }
893                } else {
894                    ComponentUtils.downloadFile(tmpFile, "clipboard.zip", "clipboardZip");
895                    tmpFile.delete();
896                }
897
898                return "";
899            }
900        } catch (IOException io) {
901            throw new NuxeoException(io);
902        }
903    }
904}