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