001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (C) Alkacon Software (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.components;
029
030import static org.opencms.ui.components.CmsResourceTableProperty.PROPERTY_CACHE;
031import static org.opencms.ui.components.CmsResourceTableProperty.PROPERTY_COPYRIGHT;
032import static org.opencms.ui.components.CmsResourceTableProperty.PROPERTY_DATE_CREATED;
033import static org.opencms.ui.components.CmsResourceTableProperty.PROPERTY_DATE_EXPIRED;
034import static org.opencms.ui.components.CmsResourceTableProperty.PROPERTY_DATE_MODIFIED;
035import static org.opencms.ui.components.CmsResourceTableProperty.PROPERTY_DATE_RELEASED;
036import static org.opencms.ui.components.CmsResourceTableProperty.PROPERTY_INSIDE_PROJECT;
037import static org.opencms.ui.components.CmsResourceTableProperty.PROPERTY_INTERNAL_RESOURCE_TYPE;
038import static org.opencms.ui.components.CmsResourceTableProperty.PROPERTY_IN_NAVIGATION;
039import static org.opencms.ui.components.CmsResourceTableProperty.PROPERTY_IS_FOLDER;
040import static org.opencms.ui.components.CmsResourceTableProperty.PROPERTY_NAVIGATION_POSITION;
041import static org.opencms.ui.components.CmsResourceTableProperty.PROPERTY_NAVIGATION_TEXT;
042import static org.opencms.ui.components.CmsResourceTableProperty.PROPERTY_PERMISSIONS;
043import static org.opencms.ui.components.CmsResourceTableProperty.PROPERTY_PROJECT;
044import static org.opencms.ui.components.CmsResourceTableProperty.PROPERTY_RELEASED_NOT_EXPIRED;
045import static org.opencms.ui.components.CmsResourceTableProperty.PROPERTY_RESOURCE_NAME;
046import static org.opencms.ui.components.CmsResourceTableProperty.PROPERTY_RESOURCE_TYPE;
047import static org.opencms.ui.components.CmsResourceTableProperty.PROPERTY_SIZE;
048import static org.opencms.ui.components.CmsResourceTableProperty.PROPERTY_STATE;
049import static org.opencms.ui.components.CmsResourceTableProperty.PROPERTY_STATE_NAME;
050import static org.opencms.ui.components.CmsResourceTableProperty.PROPERTY_TITLE;
051import static org.opencms.ui.components.CmsResourceTableProperty.PROPERTY_TYPE_ICON;
052import static org.opencms.ui.components.CmsResourceTableProperty.PROPERTY_USER_CREATED;
053import static org.opencms.ui.components.CmsResourceTableProperty.PROPERTY_USER_LOCKED;
054import static org.opencms.ui.components.CmsResourceTableProperty.PROPERTY_USER_MODIFIED;
055
056import org.opencms.db.CmsResourceState;
057import org.opencms.file.CmsObject;
058import org.opencms.file.CmsResource;
059import org.opencms.file.CmsResourceFilter;
060import org.opencms.file.CmsVfsResourceNotFoundException;
061import org.opencms.main.CmsException;
062import org.opencms.main.CmsLog;
063import org.opencms.main.OpenCms;
064import org.opencms.ui.A_CmsUI;
065import org.opencms.ui.CmsVaadinUtils;
066import org.opencms.ui.I_CmsDialogContext;
067import org.opencms.ui.I_CmsEditPropertyContext;
068import org.opencms.ui.actions.I_CmsDefaultAction;
069import org.opencms.ui.apps.CmsFileExplorerSettings;
070import org.opencms.ui.apps.I_CmsContextProvider;
071import org.opencms.ui.contextmenu.CmsContextMenu;
072import org.opencms.ui.contextmenu.CmsResourceContextMenuBuilder;
073import org.opencms.ui.contextmenu.I_CmsContextMenuBuilder;
074import org.opencms.ui.util.I_CmsItemSorter;
075import org.opencms.util.CmsStringUtil;
076import org.opencms.util.CmsUUID;
077
078import java.io.ByteArrayOutputStream;
079import java.io.OutputStreamWriter;
080import java.nio.charset.StandardCharsets;
081import java.text.DateFormat;
082import java.text.SimpleDateFormat;
083import java.util.ArrayList;
084import java.util.Arrays;
085import java.util.Collection;
086import java.util.Collections;
087import java.util.Date;
088import java.util.HashSet;
089import java.util.LinkedHashMap;
090import java.util.List;
091import java.util.Locale;
092import java.util.Map;
093import java.util.Map.Entry;
094import java.util.Set;
095import java.util.TimeZone;
096
097import org.apache.commons.logging.Log;
098
099import com.google.common.base.Function;
100import com.google.common.collect.Lists;
101import com.vaadin.event.FieldEvents.BlurEvent;
102import com.vaadin.event.FieldEvents.BlurListener;
103import com.vaadin.event.ShortcutAction.KeyCode;
104import com.vaadin.event.ShortcutListener;
105import com.vaadin.shared.MouseEventDetails.MouseButton;
106import com.vaadin.ui.Component;
107import com.vaadin.ui.themes.ValoTheme;
108import com.vaadin.v7.data.Container;
109import com.vaadin.v7.data.Container.Filter;
110import com.vaadin.v7.data.Item;
111import com.vaadin.v7.data.Property.ValueChangeEvent;
112import com.vaadin.v7.data.Property.ValueChangeListener;
113import com.vaadin.v7.data.util.DefaultItemSorter;
114import com.vaadin.v7.data.util.IndexedContainer;
115import com.vaadin.v7.data.util.filter.Or;
116import com.vaadin.v7.data.util.filter.SimpleStringFilter;
117import com.vaadin.v7.event.ItemClickEvent;
118import com.vaadin.v7.event.ItemClickEvent.ItemClickListener;
119import com.vaadin.v7.ui.AbstractTextField.TextChangeEventMode;
120import com.vaadin.v7.ui.DefaultFieldFactory;
121import com.vaadin.v7.ui.Field;
122import com.vaadin.v7.ui.Table;
123import com.vaadin.v7.ui.Table.TableDragMode;
124import com.vaadin.v7.ui.TextField;
125
126import au.com.bytecode.opencsv.CSVWriter;
127
128/**
129 * Table for displaying resources.<p>
130 */
131public class CmsFileTable extends CmsResourceTable {
132
133    /**
134     * File edit handler.<p>
135     */
136    public class FileEditHandler implements BlurListener {
137
138        /** The serial version id. */
139        private static final long serialVersionUID = -2286815522247807054L;
140
141        /**
142         * @see com.vaadin.event.FieldEvents.BlurListener#blur(com.vaadin.event.FieldEvents.BlurEvent)
143         */
144        public void blur(BlurEvent event) {
145
146            stopEdit();
147        }
148    }
149
150    /**
151     * Field factory to enable inline editing of individual file properties.<p>
152     */
153    public class FileFieldFactory extends DefaultFieldFactory {
154
155        /** The serial version id. */
156        private static final long serialVersionUID = 3079590603587933576L;
157
158        /**
159         * @see com.vaadin.ui.DefaultFieldFactory#createField(com.vaadin.v7.data.Container, java.lang.Object, java.lang.Object, com.vaadin.ui.Component)
160         */
161        @Override
162        public Field<?> createField(Container container, Object itemId, Object propertyId, Component uiContext) {
163
164            Field<?> result = null;
165            if (itemId.equals(getEditItemId().toString()) && isEditProperty((CmsResourceTableProperty)propertyId)) {
166                result = super.createField(container, itemId, propertyId, uiContext);
167                result.addStyleName(OpenCmsTheme.INLINE_TEXTFIELD);
168                result.addValidator(m_editHandler);
169                if (result instanceof TextField) {
170                    ((TextField)result).setComponentError(null);
171                    ((TextField)result).addShortcutListener(new ShortcutListener("Cancel edit", KeyCode.ESCAPE, null) {
172
173                        private static final long serialVersionUID = 1L;
174
175                        @Override
176                        public void handleAction(Object sender, Object target) {
177
178                            cancelEdit();
179                        }
180                    });
181                    ((TextField)result).addShortcutListener(new ShortcutListener("Save", KeyCode.ENTER, null) {
182
183                        private static final long serialVersionUID = 1L;
184
185                        @Override
186                        public void handleAction(Object sender, Object target) {
187
188                            stopEdit();
189                        }
190                    });
191                    ((TextField)result).addBlurListener(m_fileEditHandler);
192                    ((TextField)result).setTextChangeEventMode(TextChangeEventMode.LAZY);
193                    ((TextField)result).addTextChangeListener(m_editHandler);
194                }
195                result.focus();
196            }
197            return result;
198        }
199    }
200
201    /**
202     * Extends the default sorting to differentiate between files and folder when sorting by name.<p>
203     * Also allows sorting by navPos property for the Resource icon column.<p>
204     */
205    public static class FileSorter extends DefaultItemSorter implements I_CmsItemSorter {
206
207        /** The serial version id. */
208        private static final long serialVersionUID = 1L;
209
210        /**
211         * @see org.opencms.ui.util.I_CmsItemSorter#getSortableContainerPropertyIds(com.vaadin.v7.data.Container)
212         */
213        public Collection<?> getSortableContainerPropertyIds(Container container) {
214
215            Set<Object> result = new HashSet<Object>();
216            for (Object propId : container.getContainerPropertyIds()) {
217                Class<?> propertyType = container.getType(propId);
218                if (Comparable.class.isAssignableFrom(propertyType)
219                    || propertyType.isPrimitive()
220                    || (propId.equals(CmsResourceTableProperty.PROPERTY_TYPE_ICON)
221                        && container.getContainerPropertyIds().contains(
222                            CmsResourceTableProperty.PROPERTY_NAVIGATION_POSITION))) {
223                    result.add(propId);
224                }
225            }
226            return result;
227        }
228
229        /**
230         * @see com.vaadin.v7.data.util.DefaultItemSorter#compareProperty(java.lang.Object, boolean, com.vaadin.v7.data.Item, com.vaadin.v7.data.Item)
231         */
232        @Override
233        protected int compareProperty(Object propertyId, boolean sortDirection, Item item1, Item item2) {
234
235            //@formatter:off
236            if (CmsResourceTableProperty.PROPERTY_RESOURCE_NAME.equals(propertyId)) {
237                Boolean isFolder1 = (Boolean)item1.getItemProperty(
238                    CmsResourceTableProperty.PROPERTY_IS_FOLDER).getValue();
239                Boolean isFolder2 = (Boolean)item2.getItemProperty(
240                    CmsResourceTableProperty.PROPERTY_IS_FOLDER).getValue();
241                if (!isFolder1.equals(isFolder2)) {
242                    int result = isFolder1.booleanValue() ? -1 : 1;
243                    if (!sortDirection) {
244                        result = result * (-1);
245                    }
246                    return result;
247                }
248            } else if ((CmsResourceTableProperty.PROPERTY_TYPE_ICON.equals(propertyId)
249                || CmsResourceTableProperty.PROPERTY_NAVIGATION_TEXT.equals(propertyId))
250                && (item1.getItemProperty(CmsResourceTableProperty.PROPERTY_NAVIGATION_POSITION) != null)) {
251                    int result;
252                    Float pos1 = (Float)item1.getItemProperty(
253                        CmsResourceTableProperty.PROPERTY_NAVIGATION_POSITION).getValue();
254                    Float pos2 = (Float)item2.getItemProperty(
255                        CmsResourceTableProperty.PROPERTY_NAVIGATION_POSITION).getValue();
256                    if (pos1 == null) {
257                        result = pos2 == null
258                        ? compareProperty(CmsResourceTableProperty.PROPERTY_RESOURCE_NAME, true, item1, item2)
259                        : 1;
260                    } else {
261                        result = pos2 == null ? -1 : Float.compare(pos1.floatValue(), pos2.floatValue());
262                    }
263                    if (!sortDirection) {
264                        result = result * (-1);
265                    }
266                    return result;
267                } else if (((CmsResourceTableProperty)propertyId).getColumnType().equals(String.class)) {
268                    String value1 = (String)item1.getItemProperty(propertyId).getValue();
269                    String value2 = (String)item2.getItemProperty(propertyId).getValue();
270                    // Java collators obtained by java.text.Collator.getInstance(...) ignore spaces, and we don't want to ignore them, so we use
271                    // ICU collators instead
272                    com.ibm.icu.text.Collator collator = com.ibm.icu.text.Collator.getInstance(
273                        com.ibm.icu.util.ULocale.ROOT);
274                    int result = collator.compare(value1, value2);
275                    if (!sortDirection) {
276                        result = -result;
277                    }
278                    return result;
279                }
280            return super.compareProperty(propertyId, sortDirection, item1, item2);
281            //@formatter:on
282        }
283    }
284
285    /**
286     * Handles folder selects in the file table.<p>
287     */
288    public interface I_FolderSelectHandler {
289
290        /**
291         * Called when the folder name is left clicked.<p>
292         *
293         * @param folderId the selected folder id
294         */
295        void onFolderSelect(CmsUUID folderId);
296    }
297
298    /** The default file table columns. */
299    public static final Map<CmsResourceTableProperty, Integer> DEFAULT_TABLE_PROPERTIES;
300
301    /** The logger instance for this class. */
302    static final Log LOG = CmsLog.getLog(CmsFileTable.class);
303
304    /** The serial version id. */
305    private static final long serialVersionUID = 5460048685141699277L;
306
307    static {
308        Map<CmsResourceTableProperty, Integer> defaultProps = new LinkedHashMap<CmsResourceTableProperty, Integer>();
309        defaultProps.put(PROPERTY_TYPE_ICON, Integer.valueOf(0));
310        defaultProps.put(PROPERTY_PROJECT, Integer.valueOf(COLLAPSED));
311        defaultProps.put(PROPERTY_RESOURCE_NAME, Integer.valueOf(0));
312        defaultProps.put(PROPERTY_TITLE, Integer.valueOf(0));
313        defaultProps.put(PROPERTY_NAVIGATION_TEXT, Integer.valueOf(COLLAPSED));
314        defaultProps.put(PROPERTY_NAVIGATION_POSITION, Integer.valueOf(INVISIBLE));
315        defaultProps.put(PROPERTY_IN_NAVIGATION, Integer.valueOf(INVISIBLE));
316        defaultProps.put(PROPERTY_COPYRIGHT, Integer.valueOf(COLLAPSED));
317        defaultProps.put(PROPERTY_CACHE, Integer.valueOf(COLLAPSED));
318        defaultProps.put(PROPERTY_RESOURCE_TYPE, Integer.valueOf(0));
319        defaultProps.put(PROPERTY_INTERNAL_RESOURCE_TYPE, Integer.valueOf(COLLAPSED));
320        defaultProps.put(PROPERTY_SIZE, Integer.valueOf(0));
321        defaultProps.put(PROPERTY_PERMISSIONS, Integer.valueOf(COLLAPSED));
322        defaultProps.put(PROPERTY_DATE_MODIFIED, Integer.valueOf(0));
323        defaultProps.put(PROPERTY_USER_MODIFIED, Integer.valueOf(COLLAPSED));
324        defaultProps.put(PROPERTY_DATE_CREATED, Integer.valueOf(COLLAPSED));
325        defaultProps.put(PROPERTY_USER_CREATED, Integer.valueOf(COLLAPSED));
326        defaultProps.put(PROPERTY_DATE_RELEASED, Integer.valueOf(0));
327        defaultProps.put(PROPERTY_DATE_EXPIRED, Integer.valueOf(0));
328        defaultProps.put(PROPERTY_STATE_NAME, Integer.valueOf(0));
329        defaultProps.put(PROPERTY_USER_LOCKED, Integer.valueOf(0));
330        defaultProps.put(PROPERTY_IS_FOLDER, Integer.valueOf(INVISIBLE));
331        defaultProps.put(PROPERTY_STATE, Integer.valueOf(INVISIBLE));
332        defaultProps.put(PROPERTY_INSIDE_PROJECT, Integer.valueOf(INVISIBLE));
333        defaultProps.put(PROPERTY_RELEASED_NOT_EXPIRED, Integer.valueOf(INVISIBLE));
334        DEFAULT_TABLE_PROPERTIES = Collections.unmodifiableMap(defaultProps);
335    }
336
337    /** The selected resources. */
338    protected List<CmsResource> m_currentResources = new ArrayList<CmsResource>();
339
340    /** The default action column property. */
341    CmsResourceTableProperty m_actionColumnProperty;
342
343    /** The additional cell style generators. */
344    List<Table.CellStyleGenerator> m_additionalStyleGenerators;
345
346    /** The current file property edit handler. */
347    I_CmsFilePropertyEditHandler m_editHandler;
348
349    /** File edit event handler. */
350    FileEditHandler m_fileEditHandler = new FileEditHandler();
351
352    /** The context menu. */
353    CmsContextMenu m_menu;
354
355    /** The context menu builder. */
356    I_CmsContextMenuBuilder m_menuBuilder;
357
358    /** The table drag mode, stored during item editing. */
359    private TableDragMode m_beforEditDragMode;
360
361    /** The dialog context provider. */
362    private I_CmsContextProvider m_contextProvider;
363
364    /** The edited item id. */
365    private CmsUUID m_editItemId;
366
367    /** The edited property id. */
368    private CmsResourceTableProperty m_editProperty;
369
370    /** Saved container filters. */
371    private Collection<Filter> m_filters = Collections.emptyList();
372
373    /** The folder select handler. */
374    private I_FolderSelectHandler m_folderSelectHandler;
375
376    /** The original edit value. */
377    private String m_originalEditValue;
378
379    /**
380     * Default constructor.<p>
381     *
382     * @param contextProvider the dialog context provider
383     */
384    public CmsFileTable(I_CmsContextProvider contextProvider) {
385
386        this(contextProvider, DEFAULT_TABLE_PROPERTIES);
387    }
388
389    /**
390     * Default constructor.<p>
391     *
392     * @param contextProvider the dialog context provider
393     * @param tableColumns the table columns to show
394     */
395    public CmsFileTable(I_CmsContextProvider contextProvider, Map<CmsResourceTableProperty, Integer> tableColumns) {
396
397        super();
398        m_additionalStyleGenerators = new ArrayList<Table.CellStyleGenerator>();
399        m_actionColumnProperty = PROPERTY_RESOURCE_NAME;
400        m_contextProvider = contextProvider;
401        m_container.setItemSorter(new FileSorter());
402        m_fileTable.addStyleName(ValoTheme.TABLE_BORDERLESS);
403        m_fileTable.addStyleName(OpenCmsTheme.SIMPLE_DRAG);
404        m_fileTable.setSizeFull();
405        m_fileTable.setColumnCollapsingAllowed(true);
406        m_fileTable.setSelectable(true);
407        m_fileTable.setMultiSelect(true);
408
409        m_fileTable.setTableFieldFactory(new FileFieldFactory());
410        ColumnBuilder builder = new ColumnBuilder();
411        for (Entry<CmsResourceTableProperty, Integer> entry : tableColumns.entrySet()) {
412            builder.column(entry.getKey(), entry.getValue().intValue());
413        }
414        builder.buildColumns();
415
416        m_fileTable.setSortContainerPropertyId(CmsResourceTableProperty.PROPERTY_RESOURCE_NAME);
417        m_menu = new CmsContextMenu();
418        m_fileTable.addValueChangeListener(new ValueChangeListener() {
419
420            private static final long serialVersionUID = 1L;
421
422            public void valueChange(ValueChangeEvent event) {
423
424                @SuppressWarnings("unchecked")
425                Set<String> selectedIds = (Set<String>)event.getProperty().getValue();
426                List<CmsResource> selectedResources = new ArrayList<CmsResource>();
427                for (String id : selectedIds) {
428                    try {
429                        CmsResource resource = A_CmsUI.getCmsObject().readResource(
430                            getUUIDFromItemID(id),
431                            CmsResourceFilter.ALL);
432                        selectedResources.add(resource);
433                    } catch (CmsException e) {
434                        LOG.error(e.getLocalizedMessage(), e);
435                    }
436
437                }
438                m_currentResources = selectedResources;
439
440                rebuildMenu();
441            }
442        });
443
444        m_fileTable.addItemClickListener(new ItemClickListener() {
445
446            private static final long serialVersionUID = 1L;
447
448            public void itemClick(ItemClickEvent event) {
449
450                handleFileItemClick(event);
451            }
452        });
453
454        m_fileTable.setCellStyleGenerator(new Table.CellStyleGenerator() {
455
456            private static final long serialVersionUID = 1L;
457
458            public String getStyle(Table source, Object itemId, Object propertyId) {
459
460                Item item = m_container.getItem(itemId);
461                String style = getStateStyle(item);
462                if (m_actionColumnProperty == propertyId) {
463                    style += " " + OpenCmsTheme.HOVER_COLUMN;
464                } else if ((CmsResourceTableProperty.PROPERTY_NAVIGATION_TEXT == propertyId)
465                    || (CmsResourceTableProperty.PROPERTY_TITLE == propertyId)) {
466                        if ((item.getItemProperty(CmsResourceTableProperty.PROPERTY_IN_NAVIGATION) != null)
467                            && ((Boolean)item.getItemProperty(
468                                CmsResourceTableProperty.PROPERTY_IN_NAVIGATION).getValue()).booleanValue()) {
469                            style += " " + OpenCmsTheme.IN_NAVIGATION;
470                        }
471                    }
472                for (Table.CellStyleGenerator generator : m_additionalStyleGenerators) {
473                    String additional = generator.getStyle(source, itemId, propertyId);
474                    if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(additional)) {
475                        style += " " + additional;
476                    }
477                }
478                return style;
479            }
480        });
481
482        m_menu.setAsTableContextMenu(m_fileTable);
483    }
484
485    /**
486     * Returns the resource state specific style name.<p>
487     *
488     * @param resourceItem the resource item
489     *
490     * @return the style name
491     */
492    public static String getStateStyle(Item resourceItem) {
493
494        String result = "";
495        if (resourceItem != null) {
496            if ((resourceItem.getItemProperty(PROPERTY_INSIDE_PROJECT) == null)
497                || ((Boolean)resourceItem.getItemProperty(PROPERTY_INSIDE_PROJECT).getValue()).booleanValue()) {
498
499                CmsResourceState state = (CmsResourceState)resourceItem.getItemProperty(
500                    CmsResourceTableProperty.PROPERTY_STATE).getValue();
501                result = getStateStyle(state);
502            } else {
503                result = OpenCmsTheme.PROJECT_OTHER;
504            }
505            if ((resourceItem.getItemProperty(PROPERTY_RELEASED_NOT_EXPIRED) != null)
506                && !((Boolean)resourceItem.getItemProperty(PROPERTY_RELEASED_NOT_EXPIRED).getValue()).booleanValue()) {
507                result += " " + OpenCmsTheme.EXPIRED;
508            }
509            if ((resourceItem.getItemProperty(CmsResourceTableProperty.PROPERTY_DISABLED) != null)
510                && ((Boolean)resourceItem.getItemProperty(
511                    CmsResourceTableProperty.PROPERTY_DISABLED).getValue()).booleanValue()) {
512                result += " " + OpenCmsTheme.DISABLED;
513            }
514        }
515        return result;
516    }
517
518    /**
519     * Adds an additional cell style generator.<p>
520     *
521     * @param styleGenerator the cell style generator
522     */
523    public void addAdditionalStyleGenerator(Table.CellStyleGenerator styleGenerator) {
524
525        m_additionalStyleGenerators.add(styleGenerator);
526    }
527
528    /**
529     * Applies settings generally used within workplace app file lists.<p>
530     */
531    public void applyWorkplaceAppSettings() {
532
533        // add site path property to container
534        m_container.addContainerProperty(
535            CmsResourceTableProperty.PROPERTY_SITE_PATH,
536            CmsResourceTableProperty.PROPERTY_SITE_PATH.getColumnType(),
537            CmsResourceTableProperty.PROPERTY_SITE_PATH.getDefaultValue());
538
539        // replace the resource name column with the path column
540        Object[] visibleCols = m_fileTable.getVisibleColumns();
541        for (int i = 0; i < visibleCols.length; i++) {
542            if (CmsResourceTableProperty.PROPERTY_RESOURCE_NAME.equals(visibleCols[i])) {
543                visibleCols[i] = CmsResourceTableProperty.PROPERTY_SITE_PATH;
544            }
545        }
546        m_fileTable.setVisibleColumns(visibleCols);
547        m_fileTable.setColumnCollapsible(CmsResourceTableProperty.PROPERTY_SITE_PATH, false);
548        m_fileTable.setColumnHeader(
549            CmsResourceTableProperty.PROPERTY_SITE_PATH,
550            CmsVaadinUtils.getMessageText(CmsResourceTableProperty.PROPERTY_SITE_PATH.getHeaderKey()));
551
552        // update column visibility according to the latest file explorer settings
553        CmsFileExplorerSettings settings;
554        try {
555            settings = OpenCms.getWorkplaceAppManager().getAppSettings(
556                A_CmsUI.getCmsObject(),
557                CmsFileExplorerSettings.class);
558
559            setTableState(settings);
560        } catch (Exception e) {
561            LOG.error("Error while reading file explorer settings from user.", e);
562        }
563        m_fileTable.setSortContainerPropertyId(CmsResourceTableProperty.PROPERTY_SITE_PATH);
564        setActionColumnProperty(CmsResourceTableProperty.PROPERTY_SITE_PATH);
565        setMenuBuilder(new CmsResourceContextMenuBuilder());
566    }
567
568    /**
569     * Clears all container filters.
570     */
571    public void clearFilters() {
572
573        IndexedContainer container = (IndexedContainer)m_fileTable.getContainerDataSource();
574        container.removeAllContainerFilters();
575    }
576
577    /**
578     * Checks if the file table has a row for the resource with the given structure id.
579     *
580     * @param structureId a structure id
581     * @return true if the file table has a row for the resource with the given id
582     */
583    public boolean containsId(CmsUUID structureId) {
584
585        return m_fileTable.getContainerDataSource().getItem("" + structureId) != null;
586    }
587
588    /**
589    * Filters the displayed resources.<p>
590    * Only resources where either the resource name, the title or the nav-text contains the given substring are shown.<p>
591    *
592    * @param search the search term
593    */
594    public void filterTable(String search) {
595
596        m_container.removeAllContainerFilters();
597        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(search)) {
598            m_container.addContainerFilter(
599                new Or(
600                    new SimpleStringFilter(CmsResourceTableProperty.PROPERTY_RESOURCE_NAME, search, true, false),
601                    new SimpleStringFilter(CmsResourceTableProperty.PROPERTY_NAVIGATION_TEXT, search, true, false),
602                    new SimpleStringFilter(CmsResourceTableProperty.PROPERTY_TITLE, search, true, false)));
603        }
604        if ((m_fileTable.getValue() != null) & !((Set<?>)m_fileTable.getValue()).isEmpty()) {
605            m_fileTable.setCurrentPageFirstItemId(((Set<?>)m_fileTable.getValue()).iterator().next());
606        }
607    }
608
609    /**
610     * Generates UTF-8 encoded CSV for currently active table columns (standard columns only).
611     *
612     * <p>Note: the generated CSV takes the active filters into account.
613     *
614     * @return the generated CSV data
615     */
616    public byte[] generateCsv() {
617
618        try {
619            Container container = m_fileTable.getContainerDataSource();
620            Object[] columnArray = m_fileTable.getVisibleColumns();
621            Set<Object> columns = new HashSet<>(Arrays.asList(columnArray));
622            LinkedHashMap<Object, Function<Object, String>> columnFormatters = new LinkedHashMap<>();
623            ByteArrayOutputStream baos = new ByteArrayOutputStream();
624            CSVWriter writer = new CSVWriter(new OutputStreamWriter(baos, StandardCharsets.UTF_8));
625            List<String> csvHeaders = new ArrayList<>();
626            List<CmsResourceTableProperty> csvColumns = new ArrayList<>();
627
628            for (Object propId : m_fileTable.getVisibleColumns()) {
629                if (propId instanceof CmsResourceTableProperty) {
630                    CmsResourceTableProperty tableProp = (CmsResourceTableProperty)propId;
631                    if (!m_fileTable.isColumnCollapsed(propId)) {
632                        Class<?> colType = tableProp.getColumnType();
633                        // skip "widget"-valued columns - currently this is just the project flag
634                        if (!colType.getName().contains("vaadin")) {
635                            // always use English column headers, as external tools using the CSV may use the column labels as IDs
636                            String colHeader = OpenCms.getWorkplaceManager().getMessages(Locale.ENGLISH).key(
637                                tableProp.getHeaderKey());
638                            csvHeaders.add(colHeader);
639                            csvColumns.add(tableProp);
640                        }
641                    }
642                }
643            }
644
645            final String[] emptyArray = {};
646            writer.writeNext(csvHeaders.toArray(emptyArray));
647            final DateFormat iso8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
648            iso8601.setTimeZone(TimeZone.getTimeZone("UTC"));
649
650            Set<CmsResourceTableProperty> dateCols = new HashSet<>(
651                Arrays.asList(
652                    CmsResourceTableProperty.PROPERTY_DATE_CREATED,
653                    CmsResourceTableProperty.PROPERTY_DATE_MODIFIED,
654                    CmsResourceTableProperty.PROPERTY_DATE_RELEASED,
655                    CmsResourceTableProperty.PROPERTY_DATE_EXPIRED));
656            for (Object itemId : m_fileTable.getContainerDataSource().getItemIds()) {
657                Item item = m_fileTable.getContainerDataSource().getItem(itemId);
658                List<String> row = new ArrayList<>();
659                for (CmsResourceTableProperty col : csvColumns) {
660                    Object value = item.getItemProperty(col).getValue();
661                    // render nulls as empty strings, some special "Long"-valued date columns as ISO8601 time stamps, and just use toString() for everything else
662                    String csvValue = "";
663                    if (value != null) {
664                        if (dateCols.contains(col) && (value instanceof Long)) {
665                            csvValue = iso8601.format(new Date((Long)value));
666                        } else {
667                            csvValue = value.toString();
668                        }
669                    }
670                    row.add(csvValue);
671                }
672                writer.writeNext(row.toArray(emptyArray));
673            }
674            writer.flush();
675            byte[] result = baos.toByteArray();
676            return result;
677        } catch (Exception e) {
678            LOG.error(e.getLocalizedMessage(), e);
679            return null;
680        }
681    }
682
683    /**
684     * Returns the dialog context provider.<p>
685     *
686     * @return the dialog context provider
687     */
688    public I_CmsContextProvider getContextProvider() {
689
690        return m_contextProvider;
691    }
692
693    /**
694     * Returns the index of the first visible item.<p>
695     *
696     * @return the first visible item
697     */
698    public int getFirstVisibleItemIndex() {
699
700        return m_fileTable.getCurrentPageFirstItemIndex();
701    }
702
703    /**
704     * Gets the selected structure ids.<p>
705     *
706     * @return the set of selected structure ids
707     */
708    @SuppressWarnings("unchecked")
709    public Collection<CmsUUID> getSelectedIds() {
710
711        return itemIdsToUUIDs((Collection<String>)m_fileTable.getValue());
712    }
713
714    /**
715     * Gets the list of selected resources.<p>
716     *
717     * @return the list of selected resources
718     */
719    public List<CmsResource> getSelectedResources() {
720
721        return m_currentResources;
722    }
723
724    /**
725     * Returns the current table state.<p>
726     *
727     * @return the table state
728     */
729    public CmsFileExplorerSettings getTableSettings() {
730
731        CmsFileExplorerSettings fileTableState = new CmsFileExplorerSettings();
732
733        fileTableState.setSortAscending(m_fileTable.isSortAscending());
734        fileTableState.setSortColumnId((CmsResourceTableProperty)m_fileTable.getSortContainerPropertyId());
735        List<CmsResourceTableProperty> collapsedCollumns = new ArrayList<CmsResourceTableProperty>();
736        Object[] visibleCols = m_fileTable.getVisibleColumns();
737        for (int i = 0; i < visibleCols.length; i++) {
738            if (m_fileTable.isColumnCollapsed(visibleCols[i])) {
739                collapsedCollumns.add((CmsResourceTableProperty)visibleCols[i]);
740            }
741        }
742        fileTableState.setCollapsedColumns(collapsedCollumns);
743        return fileTableState;
744    }
745
746    /**
747     * Handles the item selection.<p>
748     *
749     * @param itemId the selected item id
750     */
751    public void handleSelection(String itemId) {
752
753        Collection<?> selection = (Collection<?>)m_fileTable.getValue();
754        if (selection == null) {
755            m_fileTable.select(itemId);
756        } else if (!selection.contains(itemId)) {
757            m_fileTable.setValue(null);
758            m_fileTable.select(itemId);
759        }
760    }
761
762    /**
763     * Returns if a file property is being edited.<p>
764     * @return <code>true</code> if a file property is being edited
765     */
766    public boolean isEditing() {
767
768        return m_editItemId != null;
769    }
770
771    /**
772     * Returns if the given property is being edited.<p>
773     *
774     * @param propertyId the property id
775     *
776     * @return <code>true</code> if the given property is being edited
777     */
778    public boolean isEditProperty(CmsResourceTableProperty propertyId) {
779
780        return (m_editProperty != null) && m_editProperty.equals(propertyId);
781    }
782
783    /**
784     * Opens the context menu.<p>
785     *
786     * @param event the click event
787     */
788    public void openContextMenu(ItemClickEvent event) {
789
790        m_menu.openForTable(event, m_fileTable);
791    }
792
793    /**
794     * Removes the given cell style generator.<p>
795     *
796     * @param styleGenerator the cell style generator to remove
797     */
798    public void removeAdditionalStyleGenerator(Table.CellStyleGenerator styleGenerator) {
799
800        m_additionalStyleGenerators.remove(styleGenerator);
801    }
802
803    /**
804     * Restores container filters to the ones previously saved via saveFilters().
805     */
806    public void restoreFilters() {
807
808        IndexedContainer container = (IndexedContainer)m_fileTable.getContainerDataSource();
809        container.removeAllContainerFilters();
810        for (Filter filter : m_filters) {
811            container.addContainerFilter(filter);
812        }
813    }
814
815    /**
816     * Saves currently active filters.<p>
817     */
818    public void saveFilters() {
819
820        IndexedContainer container = (IndexedContainer)m_fileTable.getContainerDataSource();
821        m_filters = container.getContainerFilters();
822    }
823
824    /**
825     * Sets the default action column property.<p>
826     *
827     * @param actionColumnProperty the default action column property
828     */
829    public void setActionColumnProperty(CmsResourceTableProperty actionColumnProperty) {
830
831        m_actionColumnProperty = actionColumnProperty;
832    }
833
834    /**
835     * Sets the dialog context provider.<p>
836     *
837     * @param provider the dialog context provider
838     */
839    public void setContextProvider(I_CmsContextProvider provider) {
840
841        m_contextProvider = provider;
842    }
843
844    /**
845     * Sets the first visible item index.<p>
846     *
847     * @param i the item index
848     */
849    public void setFirstVisibleItemIndex(int i) {
850
851        m_fileTable.setCurrentPageFirstItemIndex(i);
852    }
853
854    /**
855     * Sets the folder select handler.<p>
856     *
857     * @param folderSelectHandler the folder select handler
858     */
859    public void setFolderSelectHandler(I_FolderSelectHandler folderSelectHandler) {
860
861        m_folderSelectHandler = folderSelectHandler;
862    }
863
864    /**
865     * Sets the menu builder.<p>
866     *
867     * @param builder the menu builder
868     */
869    public void setMenuBuilder(I_CmsContextMenuBuilder builder) {
870
871        m_menuBuilder = builder;
872    }
873
874    /**
875     * Sets the table state.<p>
876     *
877     * @param state the table state
878     */
879    public void setTableState(CmsFileExplorerSettings state) {
880
881        if (state != null) {
882            m_fileTable.setSortContainerPropertyId(state.getSortColumnId());
883            m_fileTable.setSortAscending(state.isSortAscending());
884            Object[] visibleCols = m_fileTable.getVisibleColumns();
885            for (int i = 0; i < visibleCols.length; i++) {
886                m_fileTable.setColumnCollapsed(visibleCols[i], state.getCollapsedColumns().contains(visibleCols[i]));
887            }
888        }
889    }
890
891    /**
892     * Starts inline editing of the given file property.<p>
893     *
894     * @param itemId the item resource structure id
895     * @param propertyId the property to edit
896     * @param editHandler the edit handler
897     */
898    public void startEdit(
899        CmsUUID itemId,
900        CmsResourceTableProperty propertyId,
901        I_CmsFilePropertyEditHandler editHandler) {
902
903        m_editItemId = itemId;
904        m_editProperty = propertyId;
905        m_originalEditValue = (String)m_container.getItem(m_editItemId.toString()).getItemProperty(
906            m_editProperty).getValue();
907        m_editHandler = editHandler;
908
909        // storing current drag mode and setting it to none to avoid text selection issues in IE11
910        m_beforEditDragMode = m_fileTable.getDragMode();
911        m_fileTable.setDragMode(TableDragMode.NONE);
912
913        m_fileTable.setEditable(true);
914    }
915
916    /**
917     * Stops the current edit process to save the changed property value.<p>
918     */
919    public void stopEdit() {
920
921        if (m_editHandler != null) {
922            String value = (String)m_container.getItem(m_editItemId.toString()).getItemProperty(
923                m_editProperty).getValue();
924            if (!value.equals(m_originalEditValue)) {
925                m_editHandler.validate(value);
926                m_editHandler.save(value);
927            } else {
928                // call cancel to ensure unlock
929                m_editHandler.cancel();
930            }
931        }
932        clearEdit();
933
934        // restoring drag mode
935        m_fileTable.setDragMode(m_beforEditDragMode);
936
937        m_beforEditDragMode = null;
938    }
939
940    /**
941     * Updates all items with ids from the given list.<p>
942     *
943     * @param ids the resource structure ids to update
944     * @param remove true if the item should be removed only
945     */
946    public void update(Collection<CmsUUID> ids, boolean remove) {
947
948        for (CmsUUID id : ids) {
949            updateItem(id, remove);
950        }
951        rebuildMenu();
952    }
953
954    /**
955     * Updates the column widths.<p>
956     *
957     * The reason this is needed is that the Vaadin table does not support minimum widths for columns,
958     * so expanding columns get squished when most of the horizontal space is used by other columns.
959     * So we try to determine whether the expanded columns would have enough space, and if not, give them a
960     * fixed width.
961     *
962     * @param estimatedSpace the estimated horizontal space available for the table.
963     */
964    public void updateColumnWidths(int estimatedSpace) {
965
966        Object[] cols = m_fileTable.getVisibleColumns();
967        List<CmsResourceTableProperty> expandCols = Lists.newArrayList();
968        int nonExpandWidth = 0;
969        int totalExpandMinWidth = 0;
970        for (Object colObj : cols) {
971            if (m_fileTable.isColumnCollapsed(colObj)) {
972                continue;
973            }
974            CmsResourceTableProperty prop = (CmsResourceTableProperty)colObj;
975            if (0 < m_fileTable.getColumnExpandRatio(prop)) {
976                expandCols.add(prop);
977                totalExpandMinWidth += getAlternativeWidthForExpandingColumns(prop);
978            } else {
979                nonExpandWidth += prop.getColumnWidth();
980            }
981        }
982        if (estimatedSpace < (totalExpandMinWidth + nonExpandWidth)) {
983            for (CmsResourceTableProperty expandCol : expandCols) {
984                m_fileTable.setColumnWidth(expandCol, getAlternativeWidthForExpandingColumns(expandCol));
985            }
986        }
987    }
988
989    /**
990     * Updates the file table sorting.<p>
991     */
992    public void updateSorting() {
993
994        m_fileTable.sort();
995    }
996
997    /**
998     * Cancels the current edit process.<p>
999     */
1000    void cancelEdit() {
1001
1002        if (m_editHandler != null) {
1003            m_editHandler.cancel();
1004        }
1005        clearEdit();
1006    }
1007
1008    /**
1009     * Returns the edit item id.<p>
1010     *
1011     * @return the edit item id
1012     */
1013    CmsUUID getEditItemId() {
1014
1015        return m_editItemId;
1016    }
1017
1018    /**
1019     * Returns the edit property id.<p>
1020     *
1021     * @return the edit property id
1022     */
1023    CmsResourceTableProperty getEditProperty() {
1024
1025        return m_editProperty;
1026    }
1027
1028    /**
1029     * Handles the file table item click.<p>
1030     *
1031     * @param event the click event
1032     */
1033    void handleFileItemClick(ItemClickEvent event) {
1034
1035        if (isEditing()) {
1036            stopEdit();
1037
1038        } else if (!event.isCtrlKey() && !event.isShiftKey()) {
1039            // don't interfere with multi-selection using control key
1040            String itemId = (String)event.getItemId();
1041            CmsUUID structureId = getUUIDFromItemID(itemId);
1042            boolean openedFolder = false;
1043            if (event.getButton().equals(MouseButton.RIGHT)) {
1044                handleSelection(itemId);
1045                openContextMenu(event);
1046            } else {
1047                if ((event.getPropertyId() == null)
1048                    || CmsResourceTableProperty.PROPERTY_TYPE_ICON.equals(event.getPropertyId())) {
1049                    handleSelection(itemId);
1050                    openContextMenu(event);
1051                } else {
1052                    if (m_actionColumnProperty.equals(event.getPropertyId())) {
1053                        Boolean isFolder = (Boolean)event.getItem().getItemProperty(
1054                            CmsResourceTableProperty.PROPERTY_IS_FOLDER).getValue();
1055                        if ((isFolder != null) && isFolder.booleanValue()) {
1056                            if (m_folderSelectHandler != null) {
1057                                m_folderSelectHandler.onFolderSelect(structureId);
1058                            }
1059                            openedFolder = true;
1060                        } else {
1061                            try {
1062                                CmsObject cms = A_CmsUI.getCmsObject();
1063                                CmsResource res = cms.readResource(structureId, CmsResourceFilter.IGNORE_EXPIRATION);
1064                                m_currentResources = Collections.singletonList(res);
1065                                I_CmsDialogContext context = m_contextProvider.getDialogContext();
1066                                I_CmsDefaultAction action = OpenCms.getWorkplaceAppManager().getDefaultAction(
1067                                    context,
1068                                    m_menuBuilder);
1069                                if (action != null) {
1070                                    action.executeAction(context);
1071                                    return;
1072                                }
1073                            } catch (CmsVfsResourceNotFoundException e) {
1074                                LOG.info(e.getLocalizedMessage(), e);
1075                            } catch (CmsException e) {
1076                                LOG.error(e.getLocalizedMessage(), e);
1077                            }
1078                        }
1079                    } else {
1080                        I_CmsDialogContext context = m_contextProvider.getDialogContext();
1081                        if ((m_currentResources.size() == 1)
1082                            && m_currentResources.get(0).getStructureId().equals(structureId)
1083                            && (context instanceof I_CmsEditPropertyContext)
1084                            && ((I_CmsEditPropertyContext)context).isPropertyEditable(event.getPropertyId())) {
1085
1086                            ((I_CmsEditPropertyContext)context).editProperty(event.getPropertyId());
1087                        }
1088                    }
1089                }
1090            }
1091            // update the item on click to show any available changes
1092            if (!openedFolder) {
1093                update(Collections.singletonList(structureId), false);
1094            }
1095        }
1096    }
1097
1098    /**
1099     * Rebuilds the context menu.<p>
1100     */
1101    void rebuildMenu() {
1102
1103        if (!getSelectedIds().isEmpty() && (m_menuBuilder != null)) {
1104            m_menu.removeAllItems();
1105            m_menuBuilder.buildContextMenu(getContextProvider().getDialogContext(), m_menu);
1106        }
1107    }
1108
1109    /**
1110     * Clears the current edit process.<p>
1111     */
1112    private void clearEdit() {
1113
1114        m_fileTable.setEditable(false);
1115        if (m_editItemId != null) {
1116            updateItem(m_editItemId, false);
1117        }
1118        m_editItemId = null;
1119        m_editProperty = null;
1120        m_editHandler = null;
1121        updateSorting();
1122    }
1123
1124    /**
1125     * Gets alternative width for expanding table columns which is used when there is not enough space for
1126     * all visible columns.<p>
1127     *
1128     * @param prop the table property
1129     * @return the alternative column width
1130     */
1131    private int getAlternativeWidthForExpandingColumns(CmsResourceTableProperty prop) {
1132
1133        if (prop.getId().equals(CmsResourceTableProperty.PROPERTY_RESOURCE_NAME.getId())) {
1134            return 200;
1135        }
1136        if (prop.getId().equals(CmsResourceTableProperty.PROPERTY_TITLE.getId())) {
1137            return 300;
1138        }
1139        if (prop.getId().equals(CmsResourceTableProperty.PROPERTY_NAVIGATION_TEXT.getId())) {
1140            return 200;
1141        }
1142        return 200;
1143    }
1144
1145    /**
1146     * Updates the given item in the file table.<p>
1147     *
1148     * @param itemId the item id
1149     * @param remove true if the item should be removed only
1150     */
1151    private void updateItem(CmsUUID itemId, boolean remove) {
1152
1153        if (remove) {
1154            String idStr = itemId != null ? itemId.toString() : null;
1155            m_container.removeItem(idStr);
1156            return;
1157        }
1158
1159        CmsObject cms = A_CmsUI.getCmsObject();
1160        try {
1161            CmsResource resource = cms.readResource(itemId, CmsResourceFilter.ALL);
1162            fillItem(cms, resource, OpenCms.getWorkplaceManager().getWorkplaceLocale(cms));
1163
1164        } catch (CmsVfsResourceNotFoundException e) {
1165            if (null != itemId) {
1166                m_container.removeItem(itemId.toString());
1167            }
1168            LOG.debug("Failed to update file table item, removing it from view.", e);
1169        } catch (CmsException e) {
1170            LOG.error(e.getLocalizedMessage(), e);
1171        }
1172    }
1173
1174}