001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.ui.apps;
029
030import org.opencms.db.CmsUserSettings;
031import org.opencms.file.CmsObject;
032import org.opencms.file.CmsProject;
033import org.opencms.file.CmsResource;
034import org.opencms.file.CmsResourceFilter;
035import org.opencms.file.CmsVfsResourceNotFoundException;
036import org.opencms.main.CmsException;
037import org.opencms.main.CmsLog;
038import org.opencms.main.OpenCms;
039import org.opencms.security.CmsPermissionSet;
040import org.opencms.ui.A_CmsUI;
041import org.opencms.ui.CmsVaadinUtils;
042import org.opencms.ui.FontOpenCms;
043import org.opencms.ui.I_CmsDialogContext;
044import org.opencms.ui.I_CmsDialogContext.ContextType;
045import org.opencms.ui.I_CmsUpdateListener;
046import org.opencms.ui.actions.CmsCopyDialogAction;
047import org.opencms.ui.actions.CmsPropertiesDialogAction;
048import org.opencms.ui.actions.I_CmsWorkplaceAction;
049import org.opencms.ui.components.A_CmsFocusShortcutListener;
050import org.opencms.ui.components.CmsErrorDialog;
051import org.opencms.ui.components.CmsFileTable;
052import org.opencms.ui.components.CmsResourceIcon;
053import org.opencms.ui.components.CmsResourceTableProperty;
054import org.opencms.ui.components.CmsToolBar;
055import org.opencms.ui.components.CmsUploadButton;
056import org.opencms.ui.components.CmsUploadButton.I_UploadListener;
057import org.opencms.ui.components.I_CmsWindowCloseListener;
058import org.opencms.ui.components.OpenCmsTheme;
059import org.opencms.ui.components.extensions.CmsUploadAreaExtension;
060import org.opencms.ui.contextmenu.CmsResourceContextMenuBuilder;
061import org.opencms.ui.dialogs.CmsCopyMoveDialog;
062import org.opencms.ui.dialogs.CmsDeleteDialog;
063import org.opencms.ui.dialogs.CmsNewDialog;
064import org.opencms.util.CmsStringUtil;
065import org.opencms.util.CmsUUID;
066import org.opencms.workplace.CmsWorkplace;
067import org.opencms.workplace.explorer.CmsResourceUtil;
068
069import java.util.ArrayList;
070import java.util.Arrays;
071import java.util.Collection;
072import java.util.Collections;
073import java.util.HashMap;
074import java.util.HashSet;
075import java.util.List;
076import java.util.Map;
077import java.util.Set;
078
079import org.apache.commons.lang.RandomStringUtils;
080import org.apache.commons.logging.Log;
081
082import com.google.common.collect.Lists;
083import com.google.common.collect.Sets;
084import com.vaadin.event.Action;
085import com.vaadin.event.FieldEvents.BlurEvent;
086import com.vaadin.event.FieldEvents.FocusEvent;
087import com.vaadin.event.ShortcutAction;
088import com.vaadin.event.ShortcutAction.KeyCode;
089import com.vaadin.event.dd.DragAndDropEvent;
090import com.vaadin.event.dd.DropHandler;
091import com.vaadin.event.dd.acceptcriteria.AcceptCriterion;
092import com.vaadin.event.dd.acceptcriteria.ServerSideCriterion;
093import com.vaadin.navigator.ViewChangeListener;
094import com.vaadin.server.Page;
095import com.vaadin.server.Sizeable.Unit;
096import com.vaadin.ui.Button;
097import com.vaadin.ui.Button.ClickEvent;
098import com.vaadin.ui.Button.ClickListener;
099import com.vaadin.ui.CssLayout;
100import com.vaadin.ui.HorizontalSplitPanel;
101import com.vaadin.ui.Notification;
102import com.vaadin.ui.UI;
103import com.vaadin.ui.themes.ValoTheme;
104import com.vaadin.v7.data.Container;
105import com.vaadin.v7.data.Item;
106import com.vaadin.v7.data.Property.ValueChangeEvent;
107import com.vaadin.v7.data.Property.ValueChangeListener;
108import com.vaadin.v7.data.util.HierarchicalContainer;
109import com.vaadin.v7.data.util.IndexedContainer;
110import com.vaadin.v7.event.FieldEvents.TextChangeEvent;
111import com.vaadin.v7.event.FieldEvents.TextChangeListener;
112import com.vaadin.v7.event.ItemClickEvent;
113import com.vaadin.v7.event.ItemClickEvent.ItemClickListener;
114import com.vaadin.v7.shared.ui.combobox.FilteringMode;
115import com.vaadin.v7.ui.AbstractSelect.AbstractSelectTargetDetails;
116import com.vaadin.v7.ui.ComboBox;
117import com.vaadin.v7.ui.HorizontalLayout;
118import com.vaadin.v7.ui.Table;
119import com.vaadin.v7.ui.Table.TableDragMode;
120import com.vaadin.v7.ui.TextField;
121import com.vaadin.v7.ui.Tree;
122import com.vaadin.v7.ui.Tree.CollapseEvent;
123import com.vaadin.v7.ui.Tree.CollapseListener;
124import com.vaadin.v7.ui.Tree.ExpandEvent;
125import com.vaadin.v7.ui.Tree.ExpandListener;
126import com.vaadin.v7.ui.Tree.ItemStyleGenerator;
127import com.vaadin.v7.ui.Tree.TreeDragMode;
128
129/**
130 * The file explorer app.<p>
131 */
132@SuppressWarnings("deprecation")
133public class CmsFileExplorer
134implements I_CmsWorkplaceApp, I_CmsCachableApp, ViewChangeListener, I_CmsWindowCloseListener, I_CmsHasShortcutActions,
135I_CmsContextProvider, CmsFileTable.I_FolderSelectHandler {
136
137    /** The drop handler for copy/move operations. */
138    public class ExplorerDropHandler implements DropHandler {
139
140        /** The serial version id. */
141        private static final long serialVersionUID = 5392136127699472654L;
142
143        /** The copy move action. */
144        transient final I_CmsWorkplaceAction m_copyMoveAction = new CmsCopyDialogAction();
145
146        /**
147         * @see com.vaadin.event.dd.DropHandler#drop(com.vaadin.event.dd.DragAndDropEvent)
148         */
149        public void drop(DragAndDropEvent dragEvent) {
150
151            try {
152                CmsExplorerDialogContext context = getContext(dragEvent);
153                if (m_copyMoveAction.isActive(context)) {
154                    CmsCopyMoveDialog dialog = new CmsCopyMoveDialog(
155                        context,
156                        CmsCopyMoveDialog.DialogMode.copy_and_move);
157                    dialog.setTargetFolder(getTargetId(dragEvent));
158                    context.start(
159                        CmsVaadinUtils.getMessageText(org.opencms.ui.Messages.GUI_DIALOGTITLE_COPYMOVE_0),
160                        dialog);
161                }
162            } catch (Exception e) {
163                LOG.error("Moving resource failed", e);
164            }
165
166        }
167
168        /**
169         * @see com.vaadin.event.dd.DropHandler#getAcceptCriterion()
170         */
171        public AcceptCriterion getAcceptCriterion() {
172
173            return new ServerSideCriterion() {
174
175                private static final long serialVersionUID = 1L;
176
177                public boolean accept(DragAndDropEvent dragEvent) {
178
179                    try {
180                        if (!m_copyMoveAction.isActive(getContext(dragEvent))) {
181                            return false;
182                        }
183                    } catch (CmsException e) {
184                        LOG.error("Drag an drop evaluation failed", e);
185                        return false;
186                    }
187                    CmsUUID targetId = getTargetId(dragEvent);
188                    return mayDrop(targetId);
189                }
190            };
191        }
192
193        /**
194         * Returns the drag target id.<p>
195         *
196         * @param dragEvent the drag event
197         *
198         * @return the drag target id
199         */
200        protected CmsUUID getTargetId(DragAndDropEvent dragEvent) {
201
202            CmsUUID targetId = null;
203            if (dragEvent.getTargetDetails() instanceof AbstractSelectTargetDetails) {
204                AbstractSelectTargetDetails target = (AbstractSelectTargetDetails)dragEvent.getTargetDetails();
205                Object itemOverId = target.getItemIdOver();
206                if (itemOverId instanceof CmsUUID) {
207                    targetId = (CmsUUID)itemOverId;
208                } else if (itemOverId instanceof String) {
209                    targetId = m_fileTable.getUUIDFromItemID((String)itemOverId);
210                }
211            }
212            try {
213                CmsObject cms = A_CmsUI.getCmsObject();
214                CmsResource target = cms.readResource(targetId);
215                if (target.isFile()) {
216                    targetId = null;
217                }
218            } catch (CmsException e) {
219                targetId = null;
220                LOG.debug("Checking drop target failed, use current folder.", e);
221            }
222
223            if (targetId == null) {
224                targetId = getCurrentFolder();
225            }
226            return targetId;
227        }
228
229        /**
230         * Evaluates if a drop on the given target is allowed.<p>
231         *
232         * @param targetId the target id
233         *
234         * @return <code>true</code> if the resources may be dropped to the given target
235         */
236        protected boolean mayDrop(CmsUUID targetId) {
237
238            boolean result = false;
239            try {
240                CmsObject cms = A_CmsUI.getCmsObject();
241                CmsResource target = cms.readResource(targetId);
242                result = cms.hasPermissions(
243                    target,
244                    CmsPermissionSet.ACCESS_WRITE,
245                    false,
246                    CmsResourceFilter.ONLY_VISIBLE_NO_DELETED);
247            } catch (Exception e) {
248                LOG.debug("Checking folder write permissions failed", e);
249            }
250            return result;
251        }
252
253        /**
254         * Returns the dialog context to use.<p>
255         *
256         * @param dragEvent the drag event
257         *
258         * @return the dialog context
259         *
260         * @throws CmsException if reading the drag resource fails
261         */
262        CmsExplorerDialogContext getContext(DragAndDropEvent dragEvent) throws CmsException {
263
264            List<CmsResource> resources;
265            if ((dragEvent.getTransferable().getSourceComponent() instanceof Table)
266                && !m_fileTable.getSelectedResources().isEmpty()) {
267                resources = m_fileTable.getSelectedResources();
268            } else {
269                CmsObject cms = A_CmsUI.getCmsObject();
270                CmsUUID sourceId = m_fileTable.getUUIDFromItemID((String)dragEvent.getTransferable().getData("itemId"));
271                CmsResource source = cms.readResource(sourceId);
272                resources = Collections.singletonList(source);
273            }
274            CmsExplorerDialogContext context = new CmsExplorerDialogContext(
275                ContextType.fileTable,
276                m_fileTable,
277                CmsFileExplorer.this,
278                resources);
279            return context;
280        }
281    }
282
283    /**
284     * File tree expand listener.<p>
285     */
286    public class TreeExpandListener implements ExpandListener {
287
288        /** The serial version id. */
289        private static final long serialVersionUID = 1L;
290        /**
291         * The path fragment being opened.
292         * Will override folder visibility in case the the target path is visible to the user.
293         **/
294        private String m_openPathFragment;
295
296        /**
297         * @see com.vaadin.ui.Tree.ExpandListener#nodeExpand(com.vaadin.ui.Tree.ExpandEvent)
298         */
299        public void nodeExpand(ExpandEvent event) {
300
301            selectTreeItem((CmsUUID)event.getItemId());
302            readTreeLevel((CmsUUID)event.getItemId(), m_openPathFragment);
303        }
304
305        /**
306         * Sets the open path fragment.<p>
307         *
308         * @param openPathFragment the open path fragment
309         */
310        public void setOpenPathFragment(String openPathFragment) {
311
312            m_openPathFragment = openPathFragment;
313        }
314    }
315
316    /** Bean representing the file explorer navigation substate. */
317    static class StateBean {
318
319        /** Current folder. */
320        private String m_folder;
321
322        /** Project id. */
323        private String m_projectId;
324
325        /**selected resource.*/
326        private String m_selectedResource;
327
328        /** The site root. */
329        private String m_siteRoot;
330
331        /**
332         * Creates a new state bean.<p>
333         *
334         * @param siteRoot the site root
335         * @param folder the folder
336         * @param projectId the project id
337         */
338        public StateBean(String siteRoot, String folder, String projectId) {
339
340            m_siteRoot = siteRoot;
341            m_folder = folder;
342            m_projectId = projectId;
343            if ("".equals(m_siteRoot)) {
344                m_siteRoot = "/";
345            }
346        }
347
348        /**
349         * Parses the state bean from a string.<p>
350         *
351         * @param state the state string
352         * @return the state bean
353         */
354        public static StateBean parse(String state) {
355
356            List<String> fields = CmsStringUtil.splitAsList(state, A_CmsWorkplaceApp.PARAM_SEPARATOR);
357            if (fields.size() >= 3) {
358                String projectId = fields.get(0);
359                String siteRoot = fields.get(1);
360                String folder = fields.get(2);
361                StateBean ret = new StateBean(siteRoot, folder, projectId);
362                if (fields.size() == 4) {
363                    ret.setSelectedResource(fields.get(3));
364                }
365                return ret;
366            } else {
367                return new StateBean(null, null, null);
368            }
369        }
370
371        /**
372         * Converts state bean to a string.<p>
373         *
374         * @return the string format of the state
375         */
376        public String asString() {
377
378            String result = m_projectId
379                + A_CmsWorkplaceApp.PARAM_SEPARATOR
380                + m_siteRoot
381                + A_CmsWorkplaceApp.PARAM_SEPARATOR
382                + m_folder
383                + A_CmsWorkplaceApp.PARAM_SEPARATOR;
384            return result;
385        }
386
387        /**
388         * Returns the folderId.<p>
389         *
390         * @return the folderId
391         */
392        public String getFolder() {
393
394            return m_folder;
395        }
396
397        /**
398         * Returns the projectId.<p>
399         *
400         * @return the projectId
401         */
402        public String getProjectId() {
403
404            return m_projectId;
405        }
406
407        /**
408         * Returns the resource to select, empty if no one was set.<p>
409         *
410         * @return UUID as string
411         */
412        public String getSelectedResource() {
413
414            return m_selectedResource == null ? "" : m_selectedResource;
415        }
416
417        /**
418         * Returns the siteRoot.<p>
419         *
420         * @return the siteRoot
421         */
422        public String getSiteRoot() {
423
424            return m_siteRoot;
425        }
426
427        /**
428         * Sets a resource to be selected.<p>
429         *
430         * @param resource to get selected
431         */
432        public void setSelectedResource(String resource) {
433
434            m_selectedResource = resource;
435        }
436    }
437
438    /** The file explorer attribute key. */
439    public static final String ATTR_KEY = "CmsFileExplorer";
440
441    /** The in line editable resource properties. */
442    public static final Collection<CmsResourceTableProperty> INLINE_EDIT_PROPERTIES = Arrays.asList(
443        CmsResourceTableProperty.PROPERTY_RESOURCE_NAME,
444        CmsResourceTableProperty.PROPERTY_TITLE,
445        CmsResourceTableProperty.PROPERTY_NAVIGATION_TEXT,
446        CmsResourceTableProperty.PROPERTY_COPYRIGHT,
447        CmsResourceTableProperty.PROPERTY_CACHE);
448
449    /** The initial split position between folder tree and file table. */
450    public static final int LAYOUT_SPLIT_POSITION = 399;
451
452    /** The opened paths session attribute name. */
453    public static final String OPENED_PATHS = "explorer-opened-paths";
454
455    /** Site selector caption property. */
456    public static final String SITE_CAPTION = "site_caption";
457
458    /** Site selector site root property. */
459    public static final String SITE_ROOT = "site_root";
460
461    /** Threshold for updating the complete folder after file changes. */
462    public static final int UPDATE_FOLDER_THRESHOLD = 200;
463
464    /** Logger instance for this class. */
465    static final Log LOG = CmsLog.getLog(CmsFileExplorer.class);
466
467    /** The delete shortcut. */
468    private static final Action ACTION_DELETE = new ShortcutAction("Del", ShortcutAction.KeyCode.DELETE, null);
469
470    /** The open parent folder shortcut. */
471    private static final Action ACTION_FOLDER_UP = new ShortcutAction(
472        "Alt+ArrowUp",
473        ShortcutAction.KeyCode.ARROW_UP,
474        new int[] {ShortcutAction.ModifierKey.ALT});
475
476    /** The edit properties shortcut. */
477    private static final Action ACTION_PROPERTIES = new ShortcutAction(
478        "Alt+Enter",
479        ShortcutAction.KeyCode.ENTER,
480        new int[] {ShortcutAction.ModifierKey.ALT});
481
482    /** The rename shortcut. */
483    private static final Action ACTION_RENAME = new ShortcutAction("F2", ShortcutAction.KeyCode.F2, null);
484
485    /** The select all shortcut. */
486    private static final Action ACTION_SELECT_ALL = new ShortcutAction(
487        "Ctrl+A",
488        ShortcutAction.KeyCode.A,
489        new int[] {ShortcutAction.ModifierKey.CTRL});
490
491    /** The select all shortcut, (using Apple CMD as modifier). */
492    private static final Action ACTION_SELECT_ALL_CMD = new ShortcutAction(
493        "CMD+A",
494        ShortcutAction.KeyCode.A,
495        new int[] {ShortcutAction.ModifierKey.META});
496
497    /** The switch online shortcut. */
498    private static final Action ACTION_SWITCH_ONLINE = new ShortcutAction(
499        "Ctrl+O",
500        ShortcutAction.KeyCode.O,
501        new int[] {ShortcutAction.ModifierKey.CTRL});
502
503    /** The switch online shortcut, (using Apple CMD as modifier). */
504    private static final Action ACTION_SWITCH_ONLINE_CMD = new ShortcutAction(
505        "CMD+O",
506        ShortcutAction.KeyCode.O,
507        new int[] {ShortcutAction.ModifierKey.META});
508
509    /** The files and folder resource filter. */
510    private static final CmsResourceFilter FILES_N_FOLDERS = CmsResourceFilter.ONLY_VISIBLE;
511
512    /** The folders resource filter. */
513    private static final CmsResourceFilter FOLDERS = CmsResourceFilter.ONLY_VISIBLE_NO_DELETED.addRequireFolder();
514
515    /** The serial version id. */
516    private static final long serialVersionUID = 1L;
517
518    /** The UI context. */
519    protected I_CmsAppUIContext m_appContext;
520
521    /** Saved explorer state used by dialogs after they have finished. */
522    protected String m_savedExplorerState = "";
523
524    /** The table containing the contents of the current folder. */
525    CmsFileTable m_fileTable;
526
527    /** The info path. */
528    TextField m_infoPath;
529
530    /** The explorer shortcuts. */
531    Map<Action, Runnable> m_shortcutActions;
532
533    /** The bread crumb click listener. */
534    private ClickListener m_crumbListener;
535
536    /** The path bread crumb container. */
537    private CssLayout m_crumbs;
538
539    /** The currently viewed folder. */
540    private CmsUUID m_currentFolder;
541
542    /** The current app state. */
543    private String m_currentState;
544
545    /** The tree expand listener. */
546    private TreeExpandListener m_expandListener;
547
548    /** The folder tree. */
549    private Tree m_fileTree;
550
551    /** The first visible file table item index. */
552    private int m_firstVisibleTableItemIndex;
553
554    /** The last context menu resources. */
555    private List<CmsResource> m_lastDialogContextResources;
556
557    /** The quick launch location cache. */
558    private CmsQuickLaunchLocationCache m_locationCache;
559
560    /** The new button. */
561    private Button m_newButton;
562
563    /** The publish button. */
564    private Button m_publishButton;
565
566    /** The search field. */
567    private TextField m_searchField;
568
569    /** the currently selected file tree folder, may be null. */
570    private CmsUUID m_selectTreeFolder;
571
572    /** The site selector. */
573    private ComboBox m_siteSelector;
574
575    /** The folder tree data container. */
576    private HierarchicalContainer m_treeContainer;
577
578    /** The upload drop area extension. */
579    private CmsUploadAreaExtension m_uploadArea;
580
581    /** The upload button. */
582    private CmsUploadButton m_uploadButton;
583
584    /**
585     * Constructor.<p>
586     */
587    public CmsFileExplorer() {
588
589        m_shortcutActions = new HashMap<Action, Runnable>();
590        m_shortcutActions.put(ACTION_DELETE, new Runnable() {
591
592            public void run() {
593
594                if (!m_fileTable.getSelectedIds().isEmpty()) {
595                    I_CmsDialogContext context1 = getDialogContext();
596                    context1.start("Delete", new CmsDeleteDialog(context1));
597                }
598            }
599        });
600
601        m_shortcutActions.put(ACTION_FOLDER_UP, new Runnable() {
602
603            public void run() {
604
605                showParentFolder();
606            }
607        });
608
609        m_shortcutActions.put(ACTION_PROPERTIES, new Runnable() {
610
611            public void run() {
612
613                I_CmsWorkplaceAction propAction = new CmsPropertiesDialogAction();
614                I_CmsDialogContext context = getDialogContext();
615                if (propAction.getVisibility(context).isActive()) {
616                    propAction.executeAction(context);
617                }
618            }
619        });
620
621        m_shortcutActions.put(ACTION_RENAME, new Runnable() {
622
623            public void run() {
624
625                CmsExplorerDialogContext context = getDialogContext();
626                if (context.isPropertyEditable(CmsResourceTableProperty.PROPERTY_RESOURCE_NAME)) {
627                    context.editProperty(CmsResourceTableProperty.PROPERTY_RESOURCE_NAME);
628                }
629            }
630        });
631        Runnable selectAll = new Runnable() {
632
633            public void run() {
634
635                m_fileTable.selectAll();
636            }
637        };
638        m_shortcutActions.put(ACTION_SELECT_ALL, selectAll);
639        m_shortcutActions.put(ACTION_SELECT_ALL_CMD, selectAll);
640
641        Runnable switchOnline = new Runnable() {
642
643            public void run() {
644
645                toggleOnlineOffline();
646            }
647        };
648        m_shortcutActions.put(ACTION_SWITCH_ONLINE, switchOnline);
649        m_shortcutActions.put(ACTION_SWITCH_ONLINE_CMD, switchOnline);
650
651        m_fileTable = new CmsFileTable(this);
652        m_fileTable.setSizeFull();
653        m_fileTable.setMenuBuilder(new CmsResourceContextMenuBuilder());
654        m_fileTable.setFolderSelectHandler(this);
655        m_uploadArea = new CmsUploadAreaExtension(m_fileTable);
656        m_uploadArea.addUploadListener(new I_UploadListener() {
657
658            public void onUploadFinished(List<String> uploadedFiles) {
659
660                updateAll(true);
661            }
662        });
663        m_treeContainer = new HierarchicalContainer();
664        addTreeContainerProperties(
665            CmsResourceTableProperty.PROPERTY_RESOURCE_NAME,
666            CmsResourceTableProperty.PROPERTY_STATE,
667            CmsResourceTableProperty.PROPERTY_TREE_CAPTION,
668            CmsResourceTableProperty.PROPERTY_INSIDE_PROJECT,
669            CmsResourceTableProperty.PROPERTY_RELEASED_NOT_EXPIRED,
670            CmsResourceTableProperty.PROPERTY_DISABLED);
671        m_fileTree = new Tree();
672        m_fileTree.addStyleName(OpenCmsTheme.SIMPLE_DRAG);
673        m_fileTree.addStyleName(OpenCmsTheme.FULL_WIDTH_PADDING);
674        m_fileTree.setWidth("100%");
675        m_fileTree.setContainerDataSource(m_treeContainer);
676        //            m_fileTree.setItemIconPropertyId(CmsResourceTableProperty.PROPERTY_TYPE_ICON_RESOURCE);
677        m_fileTree.setItemCaptionPropertyId(CmsResourceTableProperty.PROPERTY_TREE_CAPTION);
678        //        m_fileTree.setCaptionAsHtml(true);
679        m_fileTree.setHtmlContentAllowed(true);
680        m_expandListener = new TreeExpandListener();
681        m_fileTree.addExpandListener(m_expandListener);
682        m_fileTree.addCollapseListener(new CollapseListener() {
683
684            private static final long serialVersionUID = 1L;
685
686            public void nodeCollapse(CollapseEvent event) {
687
688                selectTreeItem((CmsUUID)event.getItemId());
689                clearTreeLevel((CmsUUID)event.getItemId());
690            }
691        });
692
693        m_fileTree.addItemClickListener(new ItemClickListener() {
694
695            private static final long serialVersionUID = 1L;
696
697            public void itemClick(ItemClickEvent event) {
698
699                handleFileTreeClick(event);
700            }
701        });
702
703        m_fileTree.setItemStyleGenerator(new ItemStyleGenerator() {
704
705            private static final long serialVersionUID = 1L;
706
707            public String getStyle(Tree source, Object itemId) {
708
709                return CmsFileTable.getStateStyle(source.getContainerDataSource().getItem(itemId));
710            }
711        });
712        m_fileTree.addValueChangeListener(new ValueChangeListener() {
713
714            private static final long serialVersionUID = 1L;
715
716            public void valueChange(ValueChangeEvent event) {
717
718                handleFileTreeValueChange();
719            }
720
721        });
722
723        m_fileTree.setNullSelectionAllowed(false);
724
725        // init drag and drop
726        ExplorerDropHandler handler = new ExplorerDropHandler();
727        m_fileTable.setDropHandler(handler);
728        m_fileTable.setDragMode(TableDragMode.MULTIROW);
729        m_fileTree.setDropHandler(handler);
730        m_fileTree.setDragMode(TreeDragMode.NONE);
731
732        m_siteSelector = createSiteSelect(A_CmsUI.getCmsObject());
733        m_infoPath = new TextField();
734        A_CmsFocusShortcutListener shortcutListener = new A_CmsFocusShortcutListener("Open path", KeyCode.ENTER, null) {
735
736            private static final long serialVersionUID = 1L;
737
738            @Override
739            public void blur(BlurEvent event) {
740
741                super.blur(event);
742                showCrumbs(true);
743            }
744
745            @Override
746            public void focus(FocusEvent event) {
747
748                super.focus(event);
749                showCrumbs(false);
750            }
751
752            @Override
753            public void handleAction(Object sender, Object target) {
754
755                openPath(m_infoPath.getValue());
756            }
757        };
758        shortcutListener.installOn(m_infoPath);
759
760        m_crumbs = new CssLayout();
761        m_crumbs.setPrimaryStyleName(OpenCmsTheme.CRUMBS);
762        m_crumbListener = new ClickListener() {
763
764            private static final long serialVersionUID = 1L;
765
766            public void buttonClick(ClickEvent event) {
767
768                openPath((String)event.getButton().getData());
769            }
770        };
771
772        m_searchField = new TextField();
773        m_searchField.setIcon(FontOpenCms.FILTER);
774        m_searchField.setInputPrompt(
775            Messages.get().getBundle(UI.getCurrent().getLocale()).key(Messages.GUI_EXPLORER_FILTER_0));
776        m_searchField.addStyleName(ValoTheme.TEXTFIELD_INLINE_ICON);
777        m_searchField.addTextChangeListener(new TextChangeListener() {
778
779            private static final long serialVersionUID = 1L;
780
781            public void textChange(TextChangeEvent event) {
782
783                filterTable(event.getText());
784
785            }
786        });
787
788        m_locationCache = CmsQuickLaunchLocationCache.getLocationCache(CmsAppWorkplaceUi.get().getHttpSession());
789        String startSite = CmsWorkplace.getStartSiteRoot(
790            A_CmsUI.getCmsObject(),
791            CmsAppWorkplaceUi.get().getWorkplaceSettings());
792        // remove trailing slashes
793        while (startSite.endsWith("/")) {
794            startSite = startSite.substring(0, startSite.length() - 1);
795        }
796        if (m_locationCache.getFileExplorerLocation(startSite) == null) {
797            // add the configured start folder for the start site
798            String startFolder = CmsAppWorkplaceUi.get().getWorkplaceSettings().getUserSettings().getStartFolder();
799            m_locationCache.setFileExplorerLocation(startSite, startFolder);
800        }
801    }
802
803    /**
804     * @see com.vaadin.navigator.ViewChangeListener#afterViewChange(com.vaadin.navigator.ViewChangeListener.ViewChangeEvent)
805     */
806    public void afterViewChange(ViewChangeEvent event) {
807
808        m_fileTable.setFirstVisibleItemIndex(m_firstVisibleTableItemIndex);
809    }
810
811    /**
812     * @see com.vaadin.navigator.ViewChangeListener#beforeViewChange(com.vaadin.navigator.ViewChangeListener.ViewChangeEvent)
813     */
814    public boolean beforeViewChange(ViewChangeEvent event) {
815
816        m_firstVisibleTableItemIndex = m_fileTable.getFirstVisibleItemIndex();
817
818        OpenCms.getWorkplaceAppManager().storeAppSettings(
819            A_CmsUI.getCmsObject(),
820            CmsFileExplorerSettings.class,
821            m_fileTable.getTableSettings());
822        return true;
823    }
824
825    /**
826     * Changes to the given site and path.<p>
827     *
828     * @param siteRoot the site root
829     * @param path the path inside the site
830     */
831    public void changeSite(String siteRoot, String path) {
832
833        changeSite(siteRoot, path, false);
834
835    }
836
837    /**
838     * Switches to the requested site.<p>
839     *
840     * @param siteRoot the site root
841     * @param path the folder path to open
842     * @param force force the path change, even if we are currently in the same site
843     */
844    public void changeSite(String siteRoot, String path, boolean force) {
845
846        CmsObject cms = A_CmsUI.getCmsObject();
847        String currentSiteRoot = cms.getRequestContext().getSiteRoot();
848        if (force || !currentSiteRoot.equals(siteRoot)) {
849            CmsAppWorkplaceUi.get().changeSite(siteRoot);
850            if (path == null) {
851                path = m_locationCache.getFileExplorerLocation(siteRoot);
852                if (CmsStringUtil.isEmptyOrWhitespaceOnly(siteRoot)
853                    && ((path == null) || !path.startsWith("/system"))) {
854                    // switching to the root site and previous root site folder is not below /system
855                    // -> stay in the current folder
856                    path = m_locationCache.getFileExplorerLocation(currentSiteRoot);
857                    if (path != null) {
858                        path = CmsStringUtil.joinPaths(currentSiteRoot, path);
859                    }
860                }
861            }
862            openPath(path, true);
863            Container container = m_siteSelector.getContainerDataSource();
864            for (Object id : container.getItemIds()) {
865                String key = (String)id;
866                if (CmsStringUtil.comparePaths(key, siteRoot)) {
867                    siteRoot = key;
868                    break;
869                }
870            }
871            m_siteSelector.select(siteRoot);
872        }
873    }
874
875    /**
876     * Gets all ids of resources in current folder.<p>
877     *
878     * @return the
879     */
880    public List<CmsUUID> getAllIds() {
881
882        return m_fileTable.getAllIds();
883    }
884
885    /**
886     * Returns the current folder id.<p>
887     *
888     * @return the current folder structure id
889     */
890    public CmsUUID getCurrentFolder() {
891
892        return m_currentFolder;
893    }
894
895    /**
896     * @see org.opencms.ui.apps.I_CmsContextProvider#getDialogContext()
897     */
898    public CmsExplorerDialogContext getDialogContext() {
899
900        List<CmsResource> resources = m_fileTable.getSelectedResources();
901        m_lastDialogContextResources = resources;
902        CmsExplorerDialogContext context = new CmsExplorerDialogContext(
903            ContextType.fileTable,
904            m_fileTable,
905            this,
906            resources);
907        context.setEditableProperties(INLINE_EDIT_PROPERTIES);
908        return context;
909    }
910
911    /**
912     * @see org.opencms.ui.apps.I_CmsHasShortcutActions#getShortcutActions()
913     */
914    public Map<Action, Runnable> getShortcutActions() {
915
916        return m_shortcutActions;
917    }
918
919    /**
920     * @see org.opencms.ui.apps.I_CmsWorkplaceApp#initUI(org.opencms.ui.apps.I_CmsAppUIContext)
921     */
922    public void initUI(I_CmsAppUIContext context) {
923
924        m_appContext = context;
925        m_appContext.setAttribute(ATTR_KEY, this);
926        m_appContext.setMenuDialogContext(
927            new CmsExplorerDialogContext(ContextType.appToolbar, m_fileTable, this, null));
928        HorizontalSplitPanel sp = new HorizontalSplitPanel();
929        sp.setSizeFull();
930        sp.setFirstComponent(m_fileTree);
931        CmsFileExplorerSettings settings;
932        try {
933            settings = OpenCms.getWorkplaceAppManager().getAppSettings(
934                A_CmsUI.getCmsObject(),
935                CmsFileExplorerSettings.class);
936
937            m_fileTable.setTableState(settings);
938        } catch (Exception e) {
939            LOG.error("Error while reading file explorer settings from user.", e);
940        }
941        sp.setSecondComponent(m_fileTable);
942
943        sp.setSplitPosition(LAYOUT_SPLIT_POSITION, Unit.PIXELS);
944
945        context.setAppContent(sp);
946        context.showInfoArea(true);
947        HorizontalLayout inf = new HorizontalLayout();
948        inf.setSizeFull();
949        inf.setSpacing(true);
950        inf.setMargin(true);
951        m_siteSelector.setWidth("379px");
952        inf.addComponent(m_siteSelector);
953        CssLayout crumbWrapper = new CssLayout();
954        crumbWrapper.setSizeFull();
955        crumbWrapper.setPrimaryStyleName(OpenCmsTheme.CRUMB_WRAPPER);
956        crumbWrapper.addComponent(m_crumbs);
957
958        m_infoPath.setWidth("100%");
959        crumbWrapper.addComponent(m_infoPath);
960        inf.addComponent(crumbWrapper);
961        inf.setExpandRatio(crumbWrapper, 1);
962
963        m_searchField.setWidth("200px");
964        inf.addComponent(m_searchField);
965        context.setAppInfo(inf);
966
967        initToolbarButtons(context);
968        m_fileTable.updateColumnWidths(A_CmsUI.get().getPage().getBrowserWindowWidth() - LAYOUT_SPLIT_POSITION);
969    }
970
971    /**
972     * @see org.opencms.ui.apps.I_CmsCachableApp#isCachable()
973     */
974    public boolean isCachable() {
975
976        return true;
977    }
978
979    /**
980     * @see org.opencms.ui.components.CmsFileTable.I_FolderSelectHandler#onFolderSelect(org.opencms.util.CmsUUID)
981     */
982    public void onFolderSelect(CmsUUID itemId) {
983
984        expandCurrentFolder();
985        if (m_fileTree.getItem(itemId) != null) {
986            m_fileTree.select(itemId);
987        }
988        try {
989            readFolder(itemId);
990        } catch (CmsException e) {
991            CmsErrorDialog.showErrorDialog(
992                CmsVaadinUtils.getMessageText(Messages.ERR_EXPLORER_CAN_NOT_READ_RESOURCE_1, itemId),
993                e);
994            LOG.error(e.getLocalizedMessage(), e);
995        }
996    }
997
998    /**
999     * @see org.opencms.ui.apps.I_CmsCachableApp#onRestoreFromCache()
1000     */
1001    public void onRestoreFromCache() {
1002
1003        if (m_lastDialogContextResources != null) {
1004            List<CmsUUID> updateIds = Lists.newArrayList();
1005            for (CmsResource resource : m_lastDialogContextResources) {
1006                updateIds.add(resource.getStructureId());
1007            }
1008            update(updateIds);
1009        }
1010
1011    }
1012
1013    /**
1014     * Call if site and or project have been changed.<p>
1015     *
1016     * @param project the project
1017     * @param siteRoot the site root
1018     */
1019    public void onSiteOrProjectChange(CmsProject project, String siteRoot) {
1020
1021        if ((siteRoot != null) && !siteRoot.equals(getSiteRootFromState())) {
1022            changeSite(siteRoot, null, true);
1023        } else if ((project != null) && !project.getUuid().equals(getProjectIdFromState())) {
1024            openPath(getPathFromState(), true);
1025        }
1026        m_appContext.updateOnChange();
1027        setToolbarButtonsEnabled(!CmsAppWorkplaceUi.isOnlineProject());
1028    }
1029
1030    /**
1031     * @see org.opencms.ui.apps.I_CmsWorkplaceApp#onStateChange(java.lang.String)
1032     */
1033    public void onStateChange(String state) {
1034
1035        state = normalizeState(state);
1036        if ((m_currentState == null) || !m_currentState.equals(state)) {
1037            m_currentState = state;
1038            CmsObject cms = A_CmsUI.getCmsObject();
1039            String siteRoot = getSiteRootFromState();
1040            String path = getPathFromState();
1041
1042            CmsUUID projectId = getProjectIdFromState();
1043            if ((projectId != null) && !cms.getRequestContext().getCurrentProject().getUuid().equals(projectId)) {
1044                try {
1045                    CmsProject project = cms.readProject(projectId);
1046                    cms.getRequestContext().setCurrentProject(project);
1047                    CmsAppWorkplaceUi.get().getWorkplaceSettings().setProject(project.getUuid());
1048                } catch (CmsException e) {
1049                    LOG.warn("Error reading project from history state", e);
1050                }
1051                changeSite(siteRoot, path, true);
1052            } else if ((siteRoot != null)
1053                && !CmsStringUtil.comparePaths(siteRoot, cms.getRequestContext().getSiteRoot())) {
1054                String saveState = m_currentState;
1055                changeSite(siteRoot, path);
1056                if (!getSelectionFromState(saveState).isEmpty()) {
1057                    m_fileTable.setValue(Collections.singleton(getSelectionFromState(saveState)));
1058                }
1059            } else {
1060                String saveState = m_currentState;
1061                openPath(path, true);
1062                if (!getSelectionFromState(saveState).isEmpty()) {
1063                    m_fileTable.setValue(Collections.singleton(getSelectionFromState(saveState)));
1064                }
1065            }
1066        }
1067    }
1068
1069    /**
1070     * @see org.opencms.ui.components.I_CmsWindowCloseListener#onWindowClose()
1071     */
1072    public void onWindowClose() {
1073
1074        OpenCms.getWorkplaceAppManager().storeAppSettings(
1075            A_CmsUI.getCmsObject(),
1076            CmsFileExplorerSettings.class,
1077            m_fileTable.getTableSettings());
1078    }
1079
1080    /**
1081     * Fills the file table with the resources from the given path.<p>
1082     *
1083     * @param sitePath a folder site path
1084     */
1085    public void populateFileTable(String sitePath) {
1086
1087        CmsObject cms = A_CmsUI.getCmsObject();
1088        m_searchField.clear();
1089        m_firstVisibleTableItemIndex = 0;
1090        try {
1091            List<CmsResource> folderResources = cms.readResources(sitePath, FILES_N_FOLDERS, false);
1092            m_fileTable.fillTable(cms, folderResources);
1093        } catch (CmsException e) {
1094            CmsErrorDialog.showErrorDialog(e);
1095            LOG.error(e.getLocalizedMessage(), e);
1096        }
1097    }
1098
1099    /**
1100     * Updates the table entries with the given ids.<p>
1101     *
1102     * @param ids the ids of the table entries to update
1103     */
1104    public void update(Collection<CmsUUID> ids) {
1105
1106        try {
1107            CmsResource currentFolderRes = A_CmsUI.getCmsObject().readResource(m_currentFolder, CmsResourceFilter.ALL);
1108            boolean updateFolder = m_fileTable.getItemCount() < UPDATE_FOLDER_THRESHOLD;
1109            Set<CmsUUID> removeIds = new HashSet<CmsUUID>();
1110            for (CmsUUID id : ids) {
1111                boolean remove = false;
1112                try {
1113                    CmsResource resource = A_CmsUI.getCmsObject().readResource(id, CmsResourceFilter.ALL);
1114
1115                    remove = !CmsResource.getParentFolder(resource.getRootPath()).equals(
1116                        currentFolderRes.getRootPath());
1117
1118                } catch (CmsVfsResourceNotFoundException e) {
1119                    remove = true;
1120                    LOG.debug("Could not read update resource " + id, e);
1121                }
1122                if (remove) {
1123                    removeIds.add(id);
1124                }
1125
1126            }
1127            for (CmsUUID id : ids) {
1128                updateTree(id);
1129            }
1130            if (updateFolder) {
1131                updateCurrentFolder(removeIds);
1132            } else {
1133                m_fileTable.update(removeIds, true);
1134                HashSet<CmsUUID> updateIds = new HashSet<CmsUUID>(ids);
1135                ids.removeAll(removeIds);
1136                m_fileTable.update(updateIds, false);
1137            }
1138            m_fileTable.updateSorting();
1139            m_fileTable.clearSelection();
1140        } catch (CmsException e) {
1141            CmsErrorDialog.showErrorDialog(e);
1142        }
1143        if (!CmsStringUtil.isEmptyOrWhitespaceOnly(m_searchField.getValue())) {
1144            filterTable(m_searchField.getValue());
1145        }
1146    }
1147
1148    /**
1149     * Updates display for all contents of the current folder.<p>
1150     *
1151     * @param clearFilter <code>true</code> to clear the search filter
1152     */
1153    public void updateAll(boolean clearFilter) {
1154
1155        try {
1156            readFolder(m_currentFolder, clearFilter);
1157            Set<Object> ids = Sets.newHashSet();
1158            ids.addAll(m_fileTree.getItemIds());
1159            for (Object id : ids) {
1160                updateTree((CmsUUID)id);
1161            }
1162        } catch (CmsException e) {
1163            CmsErrorDialog.showErrorDialog(
1164                CmsVaadinUtils.getMessageText(Messages.ERR_EXPLORER_CAN_NOT_READ_RESOURCE_1, m_currentFolder),
1165                e);
1166            LOG.error(e.getLocalizedMessage(), e);
1167        }
1168    }
1169
1170    /**
1171     * Updates the give tree item.<p>
1172     *
1173     * @param cms the cms context
1174     * @param id the item id
1175     */
1176    public void updateResourceInTree(CmsObject cms, CmsUUID id) {
1177
1178        try {
1179            CmsResource resource = cms.readResource(id, CmsResourceFilter.IGNORE_EXPIRATION);
1180            if (resource.isFile()) {
1181                return;
1182            }
1183            // ensuring to only add items belonging to the current site
1184            if (!resource.getRootPath().startsWith(cms.getRequestContext().getSiteRoot())) {
1185                m_treeContainer.removeItemRecursively(id);
1186                return;
1187            }
1188
1189            CmsResource parent = cms.readParentFolder(id);
1190            CmsUUID parentId = null;
1191            if (parent != null) {
1192                parentId = parent.getStructureId();
1193            } else {
1194                LOG.debug("Parent for resource '" + resource.getRootPath() + "' is null");
1195            }
1196            Item resourceItem = m_treeContainer.getItem(id);
1197            if (resourceItem != null) {
1198                // use the root path as name in case of the root item
1199                String resName = parentId == null ? resource.getRootPath() : resource.getName();
1200                resourceItem.getItemProperty(CmsResourceTableProperty.PROPERTY_RESOURCE_NAME).setValue(resName);
1201                CmsResourceUtil resUtil = new CmsResourceUtil(cms, resource);
1202                String iconHTML = CmsResourceIcon.getTreeCaptionHTML(resName, resUtil, null, false);
1203                resourceItem.getItemProperty(CmsResourceTableProperty.PROPERTY_TREE_CAPTION).setValue(iconHTML);
1204                resourceItem.getItemProperty(CmsResourceTableProperty.PROPERTY_STATE).setValue(resource.getState());
1205                if (parentId != null) {
1206                    m_treeContainer.setParent(resource.getStructureId(), parentId);
1207                }
1208            } else {
1209                addTreeItem(cms, resource, parentId, false, m_treeContainer);
1210            }
1211            m_fileTree.markAsDirty();
1212        } catch (CmsVfsResourceNotFoundException e) {
1213            m_treeContainer.removeItemRecursively(id);
1214            LOG.debug(e.getLocalizedMessage(), e);
1215        } catch (CmsException e) {
1216            CmsErrorDialog.showErrorDialog(e);
1217            LOG.error(e.getLocalizedMessage(), e);
1218        }
1219    }
1220
1221    /**
1222     * Updates the tree items with the given ids.<p>
1223     *
1224     * @param id the
1225     */
1226    public void updateTree(CmsUUID id) {
1227
1228        CmsObject cms = A_CmsUI.getCmsObject();
1229        updateResourceInTree(cms, id);
1230
1231    }
1232
1233    /**
1234     * Clears the given tree level.<p>
1235     *
1236     * @param parentId the parent id
1237     */
1238    protected void clearTreeLevel(CmsUUID parentId) {
1239
1240        // create a new list to avoid concurrent modifications
1241        Collection<?> children = m_treeContainer.getChildren(parentId);
1242        // may be null when monkey clicking
1243        if (children != null) {
1244            List<Object> childIds = new ArrayList<Object>(m_treeContainer.getChildren(parentId));
1245            for (Object childId : childIds) {
1246                m_treeContainer.removeItemRecursively(childId);
1247            }
1248        }
1249    }
1250
1251    /**
1252     * Reads the given folder.<p>
1253     *
1254     * @param folderId the folder id
1255     *
1256     * @throws CmsException in case reading the folder fails
1257     */
1258    protected void readFolder(CmsUUID folderId) throws CmsException {
1259
1260        readFolder(folderId, true);
1261    }
1262
1263    /**
1264     * Reads the given folder.<p>
1265     *
1266     * @param folderId the folder id
1267     * @param clearFilter <code>true</code> to clear the search filter
1268     *
1269     * @throws CmsException in case reading the folder fails
1270     */
1271    protected void readFolder(CmsUUID folderId, boolean clearFilter) throws CmsException {
1272
1273        CmsObject cms = A_CmsUI.getCmsObject();
1274        if (clearFilter) {
1275            m_searchField.clear();
1276        }
1277        CmsResource folder = cms.readResource(folderId, FOLDERS);
1278        m_currentFolder = folderId;
1279        String folderPath = cms.getSitePath(folder);
1280        if (OpenCms.getSiteManager().isSharedFolder(cms.getRequestContext().getSiteRoot())) {
1281            folderPath = folderPath.substring(cms.getRequestContext().getSiteRoot().length());
1282        }
1283        setPathInfo(folderPath);
1284        List<CmsResource> childResources = cms.readResources(folder, FILES_N_FOLDERS, false);
1285        m_fileTable.fillTable(cms, childResources, clearFilter);
1286        boolean hasFolderChild = false;
1287        for (CmsResource child : childResources) {
1288            if (child.isFolder()) {
1289                hasFolderChild = true;
1290                break;
1291            }
1292        }
1293        m_treeContainer.setChildrenAllowed(folderId, hasFolderChild);
1294        String sitePath = folder.getRootPath().equals(cms.getRequestContext().getSiteRoot() + "/") ? "" : folderPath;
1295        String state = new StateBean(
1296            cms.getRequestContext().getSiteRoot(),
1297            sitePath,
1298            cms.getRequestContext().getCurrentProject().getUuid().toString()).asString();
1299        if (LOG.isDebugEnabled()) {
1300            String p = RandomStringUtils.randomAlphanumeric(5) + " readFolder ";
1301            LOG.debug(p + "siteRoot = " + cms.getRequestContext().getSiteRoot());
1302            LOG.debug(p + "folder = " + folder.getRootPath());
1303            LOG.debug(p + "folderPath = " + folderPath);
1304            LOG.debug(p + "sitePath = " + sitePath);
1305            LOG.debug(p + "state = " + state);
1306            LOG.debug(p + "m_currentState = " + m_currentState);
1307            LOG.debug(p + "m_currentState.asString = " + StateBean.parse(m_currentState).asString());
1308        }
1309        if ((m_currentState == null) || !(state).equals(StateBean.parse(m_currentState).asString())) {
1310            m_currentState = state;
1311            CmsAppWorkplaceUi.get().changeCurrentAppState(m_currentState);
1312        }
1313        m_locationCache.setFileExplorerLocation(cms.getRequestContext().getSiteRoot(), sitePath);
1314        m_uploadButton.setTargetFolder(folder.getRootPath());
1315        m_uploadArea.setTargetFolder(folder.getRootPath());
1316        if (!m_fileTree.isExpanded(folderId)) {
1317            expandCurrentFolder();
1318        }
1319        if (!m_fileTree.isSelected(folderId)) {
1320            m_fileTree.select(folderId);
1321        }
1322    }
1323
1324    /**
1325     * Updates the current folder and removes the given resource items.<p>
1326     *
1327     * @param removeIds the resource item ids to remove
1328     */
1329    protected void updateCurrentFolder(Collection<CmsUUID> removeIds) {
1330
1331        m_fileTable.update(removeIds, true);
1332        CmsObject cms = A_CmsUI.getCmsObject();
1333        try {
1334            // current folder may be filtered, so we clear the filters and restore them later
1335            // to make updates work for filtered out resources
1336            m_fileTable.saveFilters();
1337            m_fileTable.clearFilters();
1338            CmsResource folder = cms.readResource(m_currentFolder, FOLDERS);
1339            List<CmsResource> childResources = cms.readResources(cms.getSitePath(folder), FILES_N_FOLDERS, false);
1340            Set<CmsUUID> ids = new HashSet<CmsUUID>();
1341            for (CmsResource child : childResources) {
1342                ids.add(child.getStructureId());
1343            }
1344            m_fileTable.update(ids, false);
1345        } catch (CmsException e) {
1346            CmsErrorDialog.showErrorDialog(e);
1347            LOG.error(e.getLocalizedMessage(), e);
1348        } finally {
1349            m_fileTable.restoreFilters();
1350        }
1351    }
1352
1353    /**
1354     * Expands the currently viewed folder in the tree.<p>
1355     */
1356    void expandCurrentFolder() {
1357
1358        if (m_currentFolder != null) {
1359            m_treeContainer.setChildrenAllowed(m_currentFolder, true);
1360            m_fileTree.expandItem(m_currentFolder);
1361        }
1362    }
1363
1364    /**
1365     * Filters the file table.<p>
1366     *
1367     * @param search the search term
1368     */
1369    void filterTable(String search) {
1370
1371        m_fileTable.filterTable(search);
1372    }
1373
1374    /**
1375     * Handles the file tree click events.<p>
1376     *
1377     * @param event the event
1378     */
1379    void handleFileTreeClick(ItemClickEvent event) {
1380
1381        Item resourceItem = m_treeContainer.getItem(event.getItemId());
1382        if ((resourceItem.getItemProperty(CmsResourceTableProperty.PROPERTY_DISABLED).getValue() == null)
1383            || !((Boolean)resourceItem.getItemProperty(
1384                CmsResourceTableProperty.PROPERTY_DISABLED).getValue()).booleanValue()) {
1385            // don't handle disabled item clicks
1386            try {
1387                readFolder((CmsUUID)event.getItemId());
1388            } catch (CmsException e) {
1389                CmsErrorDialog.showErrorDialog(
1390                    CmsVaadinUtils.getMessageText(Messages.ERR_EXPLORER_CAN_NOT_READ_RESOURCE_1, event.getItemId()),
1391                    e);
1392                LOG.error(e.getLocalizedMessage(), e);
1393            }
1394        }
1395    }
1396
1397    /**
1398     * Handles the file tree value changes.<p>
1399     */
1400    void handleFileTreeValueChange() {
1401
1402        Item resourceItem = m_treeContainer.getItem(m_fileTree.getValue());
1403        if (resourceItem != null) {
1404            if ((resourceItem.getItemProperty(CmsResourceTableProperty.PROPERTY_DISABLED).getValue() != null)
1405                && ((Boolean)resourceItem.getItemProperty(
1406                    CmsResourceTableProperty.PROPERTY_DISABLED).getValue()).booleanValue()) {
1407                // in case the folder is disabled due to missing visibility, reset the value to the previous one
1408                m_fileTree.setValue(m_selectTreeFolder);
1409
1410            } else {
1411                m_selectTreeFolder = (CmsUUID)m_fileTree.getValue();
1412            }
1413        }
1414    }
1415
1416    /**
1417     * Opens the new resource dialog.<p>
1418     */
1419    void onClickNew() {
1420
1421        try {
1422            CmsObject cms = A_CmsUI.getCmsObject();
1423
1424            CmsResource folderRes = cms.readResource(m_currentFolder, CmsResourceFilter.IGNORE_EXPIRATION);
1425            I_CmsDialogContext newDialogContext = getDialogContext();
1426
1427            CmsNewDialog newDialog = new CmsNewDialog(folderRes, newDialogContext);
1428            newDialogContext.start(
1429                CmsVaadinUtils.getMessageText(org.opencms.ui.Messages.GUI_NEWRESOURCEDIALOG_TITLE_0),
1430                newDialog);
1431
1432        } catch (Exception e) {
1433            LOG.error(e.getLocalizedMessage(), e);
1434        }
1435    }
1436
1437    /**
1438     * Opens the given site path.<p>
1439     *
1440     * @param path the path
1441     */
1442    void openPath(String path) {
1443
1444        openPath(path, false);
1445    }
1446
1447    /**
1448     * Opens the given site path.<p>
1449     *
1450     * @param path the path
1451     * @param initTree <code>true</code> in case the tree needs to be initialized
1452     */
1453    void openPath(String path, boolean initTree) {
1454
1455        if (path == null) {
1456            String siteRoot = A_CmsUI.getCmsObject().getRequestContext().getSiteRoot();
1457            path = m_locationCache.getFileExplorerLocation(siteRoot);
1458            if (path == null) {
1459                path = "";
1460            } else if (OpenCms.getSiteManager().startsWithShared(path)) {
1461                path = path.substring(siteRoot.length());
1462            }
1463        }
1464        boolean existsPath = CmsStringUtil.isNotEmptyOrWhitespaceOnly(path)
1465            && A_CmsUI.getCmsObject().existsResource(path, FILES_N_FOLDERS);
1466
1467        if (path.startsWith("/")) {
1468            // remove a leading slash to avoid an empty first path fragment
1469            path = path.substring(1);
1470        }
1471
1472        String[] pathItems = path.split("/");
1473
1474        if (initTree) {
1475            try {
1476                initFolderTree(existsPath);
1477            } catch (CmsException e) {
1478                CmsErrorDialog.showErrorDialog(
1479                    CmsVaadinUtils.getMessageText(Messages.ERR_EXPLORER_CAN_NOT_OPEN_PATH_1, path),
1480                    e);
1481                LOG.error(e.getLocalizedMessage(), e);
1482                return;
1483            }
1484        }
1485
1486        Collection<?> rootItems = m_treeContainer.rootItemIds();
1487        if (rootItems.size() != 1) {
1488            throw new RuntimeException("Illeagal state, folder tree has " + rootItems.size() + " children");
1489        }
1490
1491        CmsUUID folderId = (CmsUUID)rootItems.iterator().next();
1492        if (initTree) {
1493            if (existsPath) {
1494                m_expandListener.setOpenPathFragment(pathItems[0]);
1495            }
1496            m_fileTree.expandItem(folderId);
1497            m_expandListener.setOpenPathFragment(null);
1498        }
1499        Collection<?> children = m_treeContainer.getChildren(folderId);
1500        for (int i = 0; i < pathItems.length; i++) {
1501            if (CmsStringUtil.isEmptyOrWhitespaceOnly(pathItems[i])) {
1502                continue;
1503            }
1504            CmsUUID level = null;
1505            if (children != null) {
1506                for (Object id : children) {
1507                    if (m_treeContainer.getItem(id).getItemProperty(
1508                        CmsResourceTableProperty.PROPERTY_RESOURCE_NAME).getValue().equals(pathItems[i])) {
1509                        level = (CmsUUID)id;
1510                        m_expandListener.setOpenPathFragment(
1511                            existsPath && (i < (pathItems.length - 1)) ? pathItems[i + 1] : null);
1512                        m_fileTree.expandItem(level);
1513                        m_expandListener.setOpenPathFragment(null);
1514                        break;
1515                    }
1516                }
1517            }
1518            if ((level == null) || level.equals(folderId)) {
1519                break;
1520            }
1521            folderId = level;
1522            children = m_treeContainer.getChildren(folderId);
1523        }
1524        try {
1525            readFolder(folderId);
1526        } catch (CmsException e) {
1527            CmsErrorDialog.showErrorDialog(
1528                CmsVaadinUtils.getMessageText(Messages.ERR_EXPLORER_CAN_NOT_OPEN_PATH_1, path),
1529                e);
1530            LOG.error(e.getLocalizedMessage(), e);
1531            return;
1532        }
1533    }
1534
1535    /**
1536     * Reads the given tree level.<p>
1537     *
1538     * @param parentId the parent id
1539     * @param openPathFragment the open path fragment
1540     */
1541    void readTreeLevel(CmsUUID parentId, String openPathFragment) {
1542
1543        CmsObject cms = A_CmsUI.getCmsObject();
1544        boolean openedFragment = false;
1545        try {
1546            CmsResource parent = cms.readResource(parentId, CmsResourceFilter.IGNORE_EXPIRATION);
1547            String folderPath = cms.getSitePath(parent);
1548            List<CmsResource> folderResources = cms.readResources(folderPath, FOLDERS, false);
1549
1550            // sets the parent to leaf mode, in case no child folders are present
1551            m_treeContainer.setChildrenAllowed(parentId, !folderResources.isEmpty());
1552
1553            for (CmsResource resource : folderResources) {
1554                addTreeItem(cms, resource, parentId, false, m_treeContainer);
1555                openedFragment = openedFragment || resource.getName().equals(openPathFragment);
1556            }
1557            if (!openedFragment && (openPathFragment != null)) {
1558                CmsResource resource = cms.readResource(
1559                    CmsStringUtil.joinPaths(folderPath, openPathFragment),
1560                    CmsResourceFilter.IGNORE_EXPIRATION);
1561                addTreeItem(cms, resource, parentId, true, m_treeContainer);
1562            }
1563
1564            m_fileTree.markAsDirtyRecursive();
1565        } catch (CmsException e) {
1566            CmsErrorDialog.showErrorDialog(e);
1567            LOG.error(e.getLocalizedMessage(), e);
1568        }
1569    }
1570
1571    /**
1572     * Selects the item with the given id.<p>
1573     *
1574     * @param itemId the item id
1575     */
1576    void selectTreeItem(CmsUUID itemId) {
1577
1578        m_fileTree.select(itemId);
1579    }
1580
1581    /**
1582     * Shows and hides the path bread crumb.<p>
1583     *
1584     * @param show <code>true</code> to show the bread crumb
1585     */
1586    void showCrumbs(boolean show) {
1587
1588        if (show) {
1589            m_crumbs.removeStyleName(OpenCmsTheme.HIDDEN);
1590            CmsAppWorkplaceUi.get().enableGlobalShortcuts();
1591        } else {
1592            m_crumbs.addStyleName(OpenCmsTheme.HIDDEN);
1593            CmsAppWorkplaceUi.get().disableGlobalShortcuts();
1594        }
1595    }
1596
1597    /**
1598     * Shows the parent folder, if available.<p>
1599     */
1600    void showParentFolder() {
1601
1602        CmsUUID parentId = (CmsUUID)m_treeContainer.getParent(m_currentFolder);
1603        if (parentId != null) {
1604            Item resourceItem = m_treeContainer.getItem(parentId);
1605            if ((resourceItem.getItemProperty(CmsResourceTableProperty.PROPERTY_DISABLED).getValue() == null)
1606                || !((Boolean)resourceItem.getItemProperty(
1607                    CmsResourceTableProperty.PROPERTY_DISABLED).getValue()).booleanValue()) {
1608                // don't open disabled parent folders
1609                try {
1610                    readFolder(parentId);
1611                    m_fileTree.select(parentId);
1612                } catch (CmsException e) {
1613                    CmsErrorDialog.showErrorDialog(
1614                        CmsVaadinUtils.getMessageText(Messages.ERR_EXPLORER_CAN_NOT_READ_RESOURCE_1, parentId),
1615                        e);
1616                    LOG.error(e.getLocalizedMessage(), e);
1617                }
1618            }
1619        }
1620    }
1621
1622    /**
1623     * Toggles between the online and the offline project.<p>
1624     */
1625    void toggleOnlineOffline() {
1626
1627        CmsObject cms = A_CmsUI.getCmsObject();
1628        CmsProject targetProject = null;
1629        if (cms.getRequestContext().getCurrentProject().isOnlineProject()) {
1630            targetProject = A_CmsUI.get().getLastOfflineProject();
1631            if (targetProject == null) {
1632                CmsUserSettings userSettings = new CmsUserSettings(cms);
1633                try {
1634                    CmsProject project = cms.readProject(userSettings.getStartProject());
1635                    if (!project.isOnlineProject()
1636                        && OpenCms.getOrgUnitManager().getAllAccessibleProjects(
1637                            cms,
1638                            project.getOuFqn(),
1639                            false).contains(project)) {
1640                        targetProject = project;
1641                    }
1642                } catch (CmsException e) {
1643                    LOG.debug("Error reading user start project.", e);
1644                }
1645                if (targetProject == null) {
1646                    List<CmsProject> availableProjects = CmsVaadinUtils.getAvailableProjects(cms);
1647                    for (CmsProject project : availableProjects) {
1648                        if (!project.isOnlineProject()) {
1649                            targetProject = project;
1650                            break;
1651                        }
1652                    }
1653                }
1654            }
1655        } else {
1656            try {
1657                targetProject = cms.readProject(CmsProject.ONLINE_PROJECT_ID);
1658            } catch (CmsException e) {
1659                LOG.debug("Error reading online project.", e);
1660            }
1661        }
1662        if (targetProject != null) {
1663            A_CmsUI.get().changeProject(targetProject);
1664            onSiteOrProjectChange(targetProject, null);
1665            Notification.show(
1666                CmsVaadinUtils.getMessageText(Messages.GUI_SWITCHED_TO_PROJECT_1, targetProject.getName()));
1667        }
1668    }
1669
1670    /**
1671     * Adds the given properties to the tree container.<p>
1672     *
1673     * @param properties the properties tom add
1674     */
1675    private void addTreeContainerProperties(CmsResourceTableProperty... properties) {
1676
1677        for (CmsResourceTableProperty property : properties) {
1678            m_treeContainer.addContainerProperty(property, property.getColumnType(), property.getDefaultValue());
1679        }
1680    }
1681
1682    /**
1683     * Adds an item to the folder tree.<p>
1684     *
1685     * @param cms the cms context
1686     * @param resource the folder resource
1687     * @param parentId the parent folder id
1688     * @param disabled in case the item is disabled, used for non visible parent folders
1689     * @param container the data container
1690     */
1691    private void addTreeItem(
1692        CmsObject cms,
1693        CmsResource resource,
1694        CmsUUID parentId,
1695        boolean disabled,
1696        HierarchicalContainer container) {
1697
1698        CmsResourceUtil resUtil = new CmsResourceUtil(cms, resource);
1699        Item resourceItem = container.getItem(resource.getStructureId());
1700        if (resourceItem == null) {
1701            resourceItem = container.addItem(resource.getStructureId());
1702        }
1703
1704        // use the root path as name in case of the root item
1705        String resName = parentId == null ? resource.getRootPath() : resource.getName();
1706        resourceItem.getItemProperty(CmsResourceTableProperty.PROPERTY_RESOURCE_NAME).setValue(resName);
1707        resourceItem.getItemProperty(CmsResourceTableProperty.PROPERTY_STATE).setValue(resource.getState());
1708
1709        resourceItem.getItemProperty(CmsResourceTableProperty.PROPERTY_INSIDE_PROJECT).setValue(
1710            Boolean.valueOf(resUtil.isInsideProject()));
1711        resourceItem.getItemProperty(CmsResourceTableProperty.PROPERTY_RELEASED_NOT_EXPIRED).setValue(
1712            Boolean.valueOf(resUtil.isReleasedAndNotExpired()));
1713        String iconHTML = CmsResourceIcon.getTreeCaptionHTML(resName, resUtil, null, false);
1714
1715        resourceItem.getItemProperty(CmsResourceTableProperty.PROPERTY_TREE_CAPTION).setValue(iconHTML);
1716        if (disabled) {
1717            resourceItem.getItemProperty(CmsResourceTableProperty.PROPERTY_DISABLED).setValue(Boolean.TRUE);
1718        }
1719        if (parentId != null) {
1720            container.setChildrenAllowed(parentId, true);
1721            container.setParent(resource.getStructureId(), parentId);
1722        }
1723    }
1724
1725    /**
1726     * Creates the site selector combo box.<p>
1727     *
1728     * @param cms the current cms context
1729     *
1730     * @return the combo box
1731     */
1732    private ComboBox createSiteSelect(CmsObject cms) {
1733
1734        final IndexedContainer availableSites = CmsVaadinUtils.getAvailableSitesContainer(cms, SITE_CAPTION);
1735        ComboBox combo = new ComboBox(null, availableSites);
1736        combo.setTextInputAllowed(true);
1737        combo.setNullSelectionAllowed(false);
1738        combo.setWidth("200px");
1739        combo.setInputPrompt(
1740            Messages.get().getBundle(UI.getCurrent().getLocale()).key(Messages.GUI_EXPLORER_CLICK_TO_EDIT_0));
1741        combo.setItemCaptionPropertyId(SITE_CAPTION);
1742        combo.select(cms.getRequestContext().getSiteRoot());
1743        combo.setFilteringMode(FilteringMode.CONTAINS);
1744        combo.addValueChangeListener(new ValueChangeListener() {
1745
1746            private static final long serialVersionUID = 1L;
1747
1748            public void valueChange(ValueChangeEvent event) {
1749
1750                String value = (String)event.getProperty().getValue();
1751                if (availableSites.containsId(value)) {
1752                    changeSite(value, null);
1753                    m_appContext.updateOnChange();
1754                    availableSites.removeAllContainerFilters();
1755                }
1756            }
1757        });
1758        if (Page.getCurrent().getBrowserWindowHeight() > 650) {
1759            combo.setPageLength(20);
1760        }
1761        return combo;
1762    }
1763
1764    /**
1765     * Returns the site path from the current state.<p>
1766     *
1767     * @return the site path
1768     */
1769    private String getPathFromState() {
1770
1771        return StateBean.parse(m_currentState).getFolder();
1772    }
1773
1774    /**
1775     * Returns the project id from the current state.<p>
1776     *
1777     * @return the project id
1778     */
1779    private CmsUUID getProjectIdFromState() {
1780
1781        String projectIdStr = StateBean.parse(m_currentState).getProjectId();
1782        if (CmsUUID.isValidUUID(projectIdStr)) {
1783            return new CmsUUID(projectIdStr);
1784
1785        }
1786        return null;
1787
1788    }
1789
1790    /**
1791     * Returns selected resource UUID from state (empty if not set).<p>
1792     *
1793     * @param state current state
1794     * @return uuid as string
1795     */
1796    private String getSelectionFromState(String state) {
1797
1798        return StateBean.parse(state).getSelectedResource();
1799    }
1800
1801    /**
1802     * Returns the site root from the current state.<p>
1803     *
1804     * @return the site root
1805     */
1806    private String getSiteRootFromState() {
1807
1808        String siteRoot = StateBean.parse(m_currentState).getSiteRoot();
1809        return siteRoot;
1810    }
1811
1812    /**
1813     * Populates the folder tree.<p>
1814     *
1815     * @param showInvisible to show non visible root folder as disabled
1816     *
1817     * @throws CmsException in case reading the root folder fails
1818     */
1819    private void initFolderTree(boolean showInvisible) throws CmsException {
1820
1821        CmsObject cms = A_CmsUI.getCmsObject();
1822        m_treeContainer.removeAllItems();
1823        try {
1824            CmsResource siteRoot = cms.readResource("/", FOLDERS);
1825            addTreeItem(cms, siteRoot, null, false, m_treeContainer);
1826        } catch (CmsException e) {
1827            if (showInvisible) {
1828                CmsResource siteRoot = cms.readResource("/", CmsResourceFilter.IGNORE_EXPIRATION);
1829                addTreeItem(cms, siteRoot, null, true, m_treeContainer);
1830            } else {
1831                throw e;
1832            }
1833        }
1834
1835    }
1836
1837    /**
1838     * Initializes the app specific toolbar buttons.<p>
1839     *
1840     * @param context the UI context
1841     */
1842    private void initToolbarButtons(I_CmsAppUIContext context) {
1843
1844        m_publishButton = context.addPublishButton(new I_CmsUpdateListener<String>() {
1845
1846            public void onUpdate(List<String> updatedItems) {
1847
1848                updateAll(false);
1849            }
1850
1851        });
1852
1853        m_newButton = CmsToolBar.createButton(
1854            FontOpenCms.WAND,
1855            CmsVaadinUtils.getMessageText(Messages.GUI_NEW_RESOURCE_TITLE_0));
1856        if (CmsAppWorkplaceUi.isOnlineProject()) {
1857            m_newButton.setEnabled(false);
1858            m_newButton.setDescription(CmsVaadinUtils.getMessageText(Messages.GUI_TOOLBAR_NOT_AVAILABLE_ONLINE_0));
1859        }
1860        m_newButton.addClickListener(new ClickListener() {
1861
1862            private static final long serialVersionUID = 1L;
1863
1864            public void buttonClick(ClickEvent event) {
1865
1866                onClickNew();
1867            }
1868
1869        });
1870
1871        context.addToolbarButton(m_newButton);
1872
1873        m_uploadButton = new CmsUploadButton(FontOpenCms.UPLOAD, "/");
1874        m_uploadButton.addStyleName(ValoTheme.BUTTON_BORDERLESS);
1875        m_uploadButton.addStyleName(OpenCmsTheme.TOOLBAR_BUTTON);
1876        if (CmsAppWorkplaceUi.isOnlineProject()) {
1877            m_uploadButton.setEnabled(false);
1878            m_uploadButton.setDescription(CmsVaadinUtils.getMessageText(Messages.GUI_TOOLBAR_NOT_AVAILABLE_ONLINE_0));
1879        } else {
1880            m_uploadButton.setDescription(CmsVaadinUtils.getMessageText(Messages.GUI_UPLOAD_BUTTON_TITLE_0));
1881        }
1882        m_uploadButton.addUploadListener(new I_UploadListener() {
1883
1884            public void onUploadFinished(List<String> uploadedFiles) {
1885
1886                updateAll(true);
1887            }
1888        });
1889
1890        context.addToolbarButton(m_uploadButton);
1891    }
1892
1893    /**
1894     * Normalizes the state string. Ensuring it starts with the right number of slashes resolving an issue with the vaadin state handling.<p>
1895     *
1896     * @param state the state string
1897     *
1898     * @return the normalized state string
1899     */
1900    private String normalizeState(String state) {
1901
1902        String result = "";
1903        if (state.contains(A_CmsWorkplaceApp.PARAM_SEPARATOR)) {
1904            result = state;
1905            while (result.startsWith(CmsAppWorkplaceUi.WORKPLACE_STATE_SEPARATOR)) {
1906                result = result.substring(1);
1907            }
1908        }
1909        return result;
1910    }
1911
1912    /**
1913     * Sets the current path info.<p>
1914     *
1915     * @param path the path
1916     */
1917    private void setPathInfo(String path) {
1918
1919        m_infoPath.setValue(path);
1920
1921        // generate the path bread crumb
1922        m_crumbs.removeAllComponents();
1923        int i = path.indexOf("/");
1924        String openPath = "";
1925        while (i >= 0) {
1926            String fragment = path.substring(0, i + 1);
1927            openPath += fragment;
1928            path = path.substring(i + 1);
1929            i = path.indexOf("/");
1930            Button crumb = new Button(fragment, m_crumbListener);
1931            crumb.setData(openPath);
1932            crumb.addStyleName(ValoTheme.BUTTON_LINK);
1933            m_crumbs.addComponent(crumb);
1934        }
1935    }
1936
1937    /**
1938     * Sets the toolbar buttons enabled.<p>
1939     *
1940     * @param enabled the enabled flag
1941     */
1942    private void setToolbarButtonsEnabled(boolean enabled) {
1943
1944        m_publishButton.setEnabled(enabled);
1945        m_newButton.setEnabled(enabled);
1946        m_uploadButton.setEnabled(enabled);
1947        if (enabled) {
1948            m_publishButton.setDescription(CmsVaadinUtils.getMessageText(Messages.GUI_PUBLISH_BUTTON_TITLE_0));
1949            m_newButton.setDescription(CmsVaadinUtils.getMessageText(Messages.GUI_NEW_RESOURCE_TITLE_0));
1950            m_uploadButton.setDescription(CmsVaadinUtils.getMessageText(Messages.GUI_UPLOAD_BUTTON_TITLE_0));
1951        } else {
1952            m_publishButton.setDescription(CmsVaadinUtils.getMessageText(Messages.GUI_TOOLBAR_NOT_AVAILABLE_ONLINE_0));
1953            m_newButton.setDescription(CmsVaadinUtils.getMessageText(Messages.GUI_TOOLBAR_NOT_AVAILABLE_ONLINE_0));
1954            m_uploadButton.setDescription(CmsVaadinUtils.getMessageText(Messages.GUI_TOOLBAR_NOT_AVAILABLE_ONLINE_0));
1955        }
1956    }
1957}