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 = destFolder.isTrashed();
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 = parent.isTrashed();
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        return doc.isTrashed() || doc.getAllowedStateTransitions().contains(LifeCycleConstants.DELETE_TRANSITION);
508    }
509
510    protected void setDeleteState(DocumentModel doc) {
511        if (doc.getAllowedStateTransitions().contains(LifeCycleConstants.DELETE_TRANSITION)) {
512            doc.followTransition(LifeCycleConstants.DELETE_TRANSITION);
513        }
514    }
515
516    protected void addWarnMessage(StringBuilder sb, DocumentModel doc) {
517        if (sb.length() == 0) {
518            sb.append(messages.get("document_no_deleted_state"));
519            sb.append("'").append(doc.getTitle()).append("'");
520        } else {
521            sb.append(", '").append(doc.getTitle()).append("'");
522        }
523    }
524
525    /**
526     * Check if the container is a publish space. If this is not the case, a proxy copied to it will be recreated as a
527     * new document.
528     */
529    protected boolean isPublishSpace(DocumentModel container) {
530        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
531        Set<String> publishSpaces = schemaManager.getDocumentTypeNamesForFacet(FacetNames.PUBLISH_SPACE);
532        if (publishSpaces == null || publishSpaces.isEmpty()) {
533            publishSpaces = new HashSet<>();
534        }
535        return publishSpaces.contains(container.getType());
536    }
537
538    /**
539     * Gets the parent document under the paste should be performed.
540     * <p>
541     * Rules:
542     * <p>
543     * In general the currentDocument is the parent. Exceptions to this rule: when the currentDocument is a domain or
544     * null. If Domain then content root is the parent. If null is passed, then the JCR root is taken as parent.
545     */
546    protected DocumentModel getParent(DocumentModel currentDocument) {
547
548        if (currentDocument.isFolder()) {
549            return currentDocument;
550        }
551
552        DocumentModelList parents = navigationContext.getCurrentPath();
553        for (int i = parents.size() - 1; i >= 0; i--) {
554            DocumentModel parent = parents.get(i);
555            if (parent.isFolder()) {
556                return parent;
557            }
558        }
559
560        return null;
561    }
562
563    @Override
564    @Factory(value = "isCurrentWorkListEmpty", scope = EVENT)
565    public boolean factoryForIsCurrentWorkListEmpty() {
566        return isWorkListEmpty();
567    }
568
569    @Override
570    public boolean isWorkListEmpty() {
571        return documentsListsManager.isWorkingListEmpty(getCurrentSelectedListName());
572    }
573
574    @Override
575    public String exportWorklistAsZip() {
576        return exportWorklistAsZip(documentsListsManager.getWorkingList(getCurrentSelectedListName()));
577    }
578
579    @Override
580    public String exportAllBlobsFromWorkingListAsZip() {
581        return exportWorklistAsZip();
582    }
583
584    @Override
585    public String exportMainBlobFromWorkingListAsZip() {
586        return exportWorklistAsZip();
587    }
588
589    @Override
590    public String exportWorklistAsZip(List<DocumentModel> documents) {
591        return exportWorklistAsZip(documents, true);
592    }
593
594    public String exportWorklistAsZip(DocumentModel document) {
595        return exportWorklistAsZip(Collections.singletonList(document), true);
596    }
597
598    /**
599     * Checks if copy action is available in the context of the current Document.
600     * <p>
601     * Condition: the list of selected documents is not empty.
602     */
603    @Override
604    public boolean getCanCopy() {
605        if (navigationContext.getCurrentDocument() == null) {
606            return false;
607        }
608        return !documentsListsManager.isWorkingListEmpty(DocumentsListsManager.CURRENT_DOCUMENT_SELECTION);
609    }
610
611    /**
612     * Checks if the Paste action is available in the context of the current Document. Conditions:
613     * <p>
614     * <ul>
615     * <li>list is not empty
616     * <li>user has the needed permissions on the current document
617     * <li>the content of the list can be added as children of the current document
618     * </ul>
619     */
620    @Override
621    public boolean getCanPaste(String listName) {
622
623        DocumentModel currentDocument = navigationContext.getCurrentDocument();
624
625        if (documentsListsManager.isWorkingListEmpty(listName) || currentDocument == null) {
626            return false;
627        }
628
629        DocumentModel pasteTarget = getParent(navigationContext.getCurrentDocument());
630        if (pasteTarget == null) {
631            // parent may be unreachable (right inheritance blocked)
632            return false;
633        }
634        if (!documentManager.hasPermission(pasteTarget.getRef(), SecurityConstants.ADD_CHILDREN)) {
635            return false;
636        } else {
637            // filter on allowed content types
638            // see if at least one doc can be pasted
639            // String pasteTypeName = clipboard.getClipboardDocumentType();
640            List<String> pasteTypesName = documentsListsManager.getWorkingListTypes(listName);
641            for (String pasteTypeName : pasteTypesName) {
642                if (typeManager.isAllowedSubType(pasteTypeName, pasteTarget.getType(),
643                        navigationContext.getCurrentDocument())) {
644                    return true;
645                }
646            }
647            return false;
648        }
649    }
650
651    @Override
652    public boolean getCanPasteInside(String listName, DocumentModel document) {
653        if (documentsListsManager.isWorkingListEmpty(listName) || document == null) {
654            return false;
655        }
656
657        if (!documentManager.hasPermission(document.getRef(), SecurityConstants.ADD_CHILDREN)) {
658            return false;
659        } else {
660            // filter on allowed content types
661            // see if at least one doc can be pasted
662            // String pasteTypeName = clipboard.getClipboardDocumentType();
663            List<String> pasteTypesName = documentsListsManager.getWorkingListTypes(listName);
664            for (String pasteTypeName : pasteTypesName) {
665                if (typeManager.isAllowedSubType(pasteTypeName, document.getType(),
666                        navigationContext.getCurrentDocument())) {
667                    return true;
668                }
669            }
670            return false;
671        }
672    }
673
674    /**
675     * Checks if the Move action is available in the context of the document document. Conditions:
676     * <p>
677     * <ul>
678     * <li>list is not empty
679     * <li>user has the needed permissions on the document
680     * <li>an element in the list can be removed from its folder and added as child of the current document
681     * </ul>
682     */
683    @Override
684    public boolean getCanMoveInside(String listName, DocumentModel document) {
685        if (documentsListsManager.isWorkingListEmpty(listName) || document == null) {
686            return false;
687        }
688        DocumentRef destFolderRef = document.getRef();
689        DocumentModel destFolder = document;
690        if (!documentManager.hasPermission(destFolderRef, SecurityConstants.ADD_CHILDREN)) {
691            return false;
692        } else {
693            // filter on allowed content types
694            // see if at least one doc can be removed and pasted
695            for (DocumentModel docModel : documentsListsManager.getWorkingList(listName)) {
696                // skip deleted documents
697                if (!exists(docModel.getRef())) {
698                    continue;
699                }
700                DocumentRef sourceFolderRef = docModel.getParentRef();
701                String sourceType = docModel.getType();
702                boolean canRemoveDoc = documentManager.hasPermission(sourceFolderRef, SecurityConstants.REMOVE_CHILDREN);
703                boolean canPasteInCurrentFolder = typeManager.isAllowedSubType(sourceType, destFolder.getType(),
704                        navigationContext.getCurrentDocument());
705                boolean sameFolder = sourceFolderRef.equals(destFolderRef);
706                if (canRemoveDoc && canPasteInCurrentFolder && !sameFolder) {
707                    return true;
708                }
709            }
710            return false;
711        }
712    }
713
714    /**
715     * Checks if the Move action is available in the context of the current Document. Conditions:
716     * <p>
717     * <ul>
718     * <li>list is not empty
719     * <li>user has the needed permissions on the current document
720     * <li>an element in the list can be removed from its folder and added as child of the current document
721     * </ul>
722     */
723    public boolean getCanMove(String listName) {
724        DocumentModel currentDocument = navigationContext.getCurrentDocument();
725        return getCanMoveInside(listName, currentDocument);
726    }
727
728    @Override
729    public boolean getCanPasteWorkList() {
730        return getCanPaste(getCurrentSelectedListName());
731    }
732
733    @Override
734    public boolean getCanMoveWorkingList() {
735        return getCanMove(getCurrentSelectedListName());
736    }
737
738    @Override
739    public boolean getCanPasteFromClipboard() {
740        return getCanPaste(DocumentsListsManager.CLIPBOARD);
741    }
742
743    @Override
744    public boolean getCanPasteFromClipboardInside(DocumentModel document) {
745        return getCanPasteInside(DocumentsListsManager.CLIPBOARD, document);
746    }
747
748    @Override
749    public boolean getCanMoveFromClipboardInside(DocumentModel document) {
750        return getCanMoveInside(DocumentsListsManager.CLIPBOARD, document);
751    }
752
753    @Override
754    public void setCurrentSelectedList(String listId) {
755        if (listId != null && !listId.equals(currentSelectedList)) {
756            currentSelectedList = listId;
757            canEditSelectedDocs = null;
758        }
759    }
760
761    @RequestParameter()
762    String listIdToSelect;
763
764    @Override
765    public void selectList() {
766        if (listIdToSelect != null) {
767            setCurrentSelectedList(listIdToSelect);
768        }
769    }
770
771    @Override
772    public List<DocumentModel> getCurrentSelectedList() {
773        return documentsListsManager.getWorkingList(getCurrentSelectedListName());
774    }
775
776    @Override
777    public String getCurrentSelectedListName() {
778        if (currentSelectedList == null) {
779            if (!getAvailableLists().isEmpty()) {
780                setCurrentSelectedList(availableLists.get(0));
781            }
782        }
783        return currentSelectedList;
784    }
785
786    @Override
787    public String getCurrentSelectedListTitle() {
788        String title = null;
789        String listName = getCurrentSelectedListName();
790        if (listName != null) {
791            DocumentsListDescriptor desc = documentsListsManager.getWorkingListDescriptor(listName);
792            if (desc != null) {
793                title = desc.getTitle();
794            }
795        }
796        return title;
797    }
798
799    @Override
800    public List<String> getAvailableLists() {
801        if (availableLists == null) {
802            availableLists = documentsListsManager.getWorkingListNamesForCategory("CLIPBOARD");
803        }
804        return availableLists;
805    }
806
807    @Override
808    public List<DocumentsListDescriptor> getDescriptorsForAvailableLists() {
809        if (descriptorsForAvailableLists == null) {
810            List<String> availableLists = getAvailableLists();
811            descriptorsForAvailableLists = new ArrayList<>();
812            for (String lName : availableLists) {
813                descriptorsForAvailableLists.add(documentsListsManager.getWorkingListDescriptor(lName));
814            }
815        }
816        return descriptorsForAvailableLists;
817    }
818
819    @Override
820    public List<Action> getActionsForCurrentList() {
821        String lstName = getCurrentSelectedListName();
822        if (isWorkListEmpty()) {
823            // we use cache here since this is a very common case ...
824            if (actionCache == null) {
825                actionCache = new HashMap<>();
826            }
827            if (!actionCache.containsKey(lstName)) {
828                actionCache.put(lstName, webActions.getActionsList(lstName + "_LIST"));
829            }
830            return actionCache.get(lstName);
831        } else {
832            return webActions.getActionsList(lstName + "_LIST");
833        }
834    }
835
836    @Override
837    public List<Action> getActionsForSelection() {
838        return webActions.getActionsList(DocumentsListsManager.CURRENT_DOCUMENT_SELECTION + "_LIST", false);
839    }
840
841    private void autoSelectCurrentList(String listName) {
842        previouslySelectedList = getCurrentSelectedListName();
843        setCurrentSelectedList(listName);
844    }
845
846    private void returnToPreviouslySelectedList() {
847        setCurrentSelectedList(previouslySelectedList);
848    }
849
850    @Override
851    public boolean getCanEditSelectedDocs() {
852        if (canEditSelectedDocs == null) {
853            if (getCurrentSelectedList().isEmpty()) {
854                canEditSelectedDocs = false;
855            } else {
856                final List<DocumentModel> selectedDocs = getCurrentSelectedList();
857
858                // check selected docs
859                canEditSelectedDocs = checkWritePerm(selectedDocs);
860            }
861        }
862        return canEditSelectedDocs;
863    }
864
865    @Override
866    @Deprecated
867    // no longer used by the user_clipboard.xhtml template
868    public boolean getCanEditListDocs(String listName) {
869        final List<DocumentModel> docs = documentsListsManager.getWorkingList(listName);
870
871        final boolean canEdit;
872        if (docs.isEmpty()) {
873            canEdit = false;
874        } else {
875            // check selected docs
876            canEdit = checkWritePerm(docs);
877        }
878        return canEdit;
879    }
880
881    private boolean checkWritePerm(List<DocumentModel> selectedDocs) {
882        for (DocumentModel documentModel : selectedDocs) {
883            boolean canWrite = documentManager.hasPermission(documentModel.getRef(), SecurityConstants.WRITE_PROPERTIES);
884            if (!canWrite) {
885                return false;
886            }
887        }
888        return true;
889    }
890
891    @Override
892    public boolean isCacheEnabled() {
893        if (!SeamCacheHelper.canUseSeamCache()) {
894            return false;
895        }
896        return isWorkListEmpty();
897    }
898
899    @Override
900    public String getCacheKey() {
901        return getCurrentSelectedListName() + "::" + localeSelector.getLocaleString();
902    }
903
904    @Override
905    public boolean isCacheEnabledForSelection() {
906        if (!SeamCacheHelper.canUseSeamCache()) {
907            return false;
908        }
909        return documentsListsManager.isWorkingListEmpty(DocumentsListsManager.CURRENT_DOCUMENT_SELECTION);
910    }
911
912    @Override
913    public String exportWorklistAsZip(List<DocumentModel> documents, boolean exportAllBlobs) {
914        Blob blob = null;
915        try {
916            DownloadService downloadService = Framework.getService(DownloadService.class);
917            DocumentListZipExporter zipExporter = new DocumentListZipExporter();
918            blob = zipExporter.exportWorklistAsZip(documents, documentManager, exportAllBlobs);
919            if (blob == null) {
920                // empty zip file, do nothing
921                facesMessages.add(StatusMessage.Severity.INFO, messages.get("label.clipboard.emptyDocuments"));
922                return null;
923            }
924            blob.setMimeType("application/zip");
925            blob.setFilename("clipboard.zip");
926
927            String key = downloadService.storeBlobs(Collections.singletonList(blob));
928            String url = BaseURL.getBaseURL() + downloadService.getDownloadUrl(key);
929            ExternalContext context = FacesContext.getCurrentInstance().getExternalContext();
930            context.redirect(url);
931            return "";
932        } catch (IOException io) {
933            if (blob != null) {
934                blob.getFile().delete();
935            }
936            throw new NuxeoException("Error while redirecting for clipboard content", io);
937        }
938    }
939}