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