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