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    * Filters the displayed resources.<p>
579    * Only resources where either the resource name, the title or the nav-text contains the given substring are shown.<p>
580    *
581    * @param search the search term
582    */
583    public void filterTable(String search) {
584
585        m_container.removeAllContainerFilters();
586        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(search)) {
587            m_container.addContainerFilter(
588                new Or(
589                    new SimpleStringFilter(CmsResourceTableProperty.PROPERTY_RESOURCE_NAME, search, true, false),
590                    new SimpleStringFilter(CmsResourceTableProperty.PROPERTY_NAVIGATION_TEXT, search, true, false),
591                    new SimpleStringFilter(CmsResourceTableProperty.PROPERTY_TITLE, search, true, false)));
592        }
593        if ((m_fileTable.getValue() != null) & !((Set<?>)m_fileTable.getValue()).isEmpty()) {
594            m_fileTable.setCurrentPageFirstItemId(((Set<?>)m_fileTable.getValue()).iterator().next());
595        }
596    }
597
598    /**
599     * Generates UTF-8 encoded CSV for currently active table columns (standard columns only).
600     *
601     * <p>Note: the generated CSV takes the active filters into account.
602     *
603     * @return the generated CSV data
604     */
605    public byte[] generateCsv() {
606
607        try {
608            Container container = m_fileTable.getContainerDataSource();
609            Object[] columnArray = m_fileTable.getVisibleColumns();
610            Set<Object> columns = new HashSet<>(Arrays.asList(columnArray));
611            LinkedHashMap<Object, Function<Object, String>> columnFormatters = new LinkedHashMap<>();
612            ByteArrayOutputStream baos = new ByteArrayOutputStream();
613            CSVWriter writer = new CSVWriter(new OutputStreamWriter(baos, StandardCharsets.UTF_8));
614            List<String> csvHeaders = new ArrayList<>();
615            List<CmsResourceTableProperty> csvColumns = new ArrayList<>();
616
617            for (Object propId : m_fileTable.getVisibleColumns()) {
618                if (propId instanceof CmsResourceTableProperty) {
619                    CmsResourceTableProperty tableProp = (CmsResourceTableProperty)propId;
620                    if (!m_fileTable.isColumnCollapsed(propId)) {
621                        Class<?> colType = tableProp.getColumnType();
622                        // skip "widget"-valued columns - currently this is just the project flag
623                        if (!colType.getName().contains("vaadin")) {
624                            // always use English column headers, as external tools using the CSV may use the column labels as IDs
625                            String colHeader = OpenCms.getWorkplaceManager().getMessages(Locale.ENGLISH).key(
626                                tableProp.getHeaderKey());
627                            csvHeaders.add(colHeader);
628                            csvColumns.add(tableProp);
629                        }
630                    }
631                }
632            }
633
634            final String[] emptyArray = {};
635            writer.writeNext(csvHeaders.toArray(emptyArray));
636            final DateFormat iso8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
637            iso8601.setTimeZone(TimeZone.getTimeZone("UTC"));
638
639            Set<CmsResourceTableProperty> dateCols = new HashSet<>(
640                Arrays.asList(
641                    CmsResourceTableProperty.PROPERTY_DATE_CREATED,
642                    CmsResourceTableProperty.PROPERTY_DATE_MODIFIED,
643                    CmsResourceTableProperty.PROPERTY_DATE_RELEASED,
644                    CmsResourceTableProperty.PROPERTY_DATE_EXPIRED));
645            for (Object itemId : m_fileTable.getContainerDataSource().getItemIds()) {
646                Item item = m_fileTable.getContainerDataSource().getItem(itemId);
647                List<String> row = new ArrayList<>();
648                for (CmsResourceTableProperty col : csvColumns) {
649                    Object value = item.getItemProperty(col).getValue();
650                    // render nulls as empty strings, some special "Long"-valued date columns as ISO8601 time stamps, and just use toString() for everything else
651                    String csvValue = "";
652                    if (value != null) {
653                        if (dateCols.contains(col) && (value instanceof Long)) {
654                            csvValue = iso8601.format(new Date((Long)value));
655                        } else {
656                            csvValue = value.toString();
657                        }
658                    }
659                    row.add(csvValue);
660                }
661                writer.writeNext(row.toArray(emptyArray));
662            }
663            writer.flush();
664            byte[] result = baos.toByteArray();
665            return result;
666        } catch (Exception e) {
667            LOG.error(e.getLocalizedMessage(), e);
668            return null;
669        }
670    }
671
672    /**
673     * Returns the index of the first visible item.<p>
674     *
675     * @return the first visible item
676     */
677    public int getFirstVisibleItemIndex() {
678
679        return m_fileTable.getCurrentPageFirstItemIndex();
680    }
681
682    /**
683     * Gets the selected structure ids.<p>
684     *
685     * @return the set of selected structure ids
686     */
687    @SuppressWarnings("unchecked")
688    public Collection<CmsUUID> getSelectedIds() {
689
690        return itemIdsToUUIDs((Collection<String>)m_fileTable.getValue());
691    }
692
693    /**
694     * Gets the list of selected resources.<p>
695     *
696     * @return the list of selected resources
697     */
698    public List<CmsResource> getSelectedResources() {
699
700        return m_currentResources;
701    }
702
703    /**
704     * Returns the current table state.<p>
705     *
706     * @return the table state
707     */
708    public CmsFileExplorerSettings getTableSettings() {
709
710        CmsFileExplorerSettings fileTableState = new CmsFileExplorerSettings();
711
712        fileTableState.setSortAscending(m_fileTable.isSortAscending());
713        fileTableState.setSortColumnId((CmsResourceTableProperty)m_fileTable.getSortContainerPropertyId());
714        List<CmsResourceTableProperty> collapsedCollumns = new ArrayList<CmsResourceTableProperty>();
715        Object[] visibleCols = m_fileTable.getVisibleColumns();
716        for (int i = 0; i < visibleCols.length; i++) {
717            if (m_fileTable.isColumnCollapsed(visibleCols[i])) {
718                collapsedCollumns.add((CmsResourceTableProperty)visibleCols[i]);
719            }
720        }
721        fileTableState.setCollapsedColumns(collapsedCollumns);
722        return fileTableState;
723    }
724
725    /**
726     * Handles the item selection.<p>
727     *
728     * @param itemId the selected item id
729     */
730    public void handleSelection(String itemId) {
731
732        Collection<?> selection = (Collection<?>)m_fileTable.getValue();
733        if (selection == null) {
734            m_fileTable.select(itemId);
735        } else if (!selection.contains(itemId)) {
736            m_fileTable.setValue(null);
737            m_fileTable.select(itemId);
738        }
739    }
740
741    /**
742     * Checks if the file table has a row for the resource with the given structure id.
743     *
744     * @param structureId a structure id
745     * @return true if the file table has a row for the resource with the given id
746     */
747    public boolean containsId(CmsUUID structureId) {
748
749        return m_fileTable.getContainerDataSource().getItem("" + structureId) != null;
750    }
751
752    /**
753     * Returns if a file property is being edited.<p>
754     * @return <code>true</code> if a file property is being edited
755     */
756    public boolean isEditing() {
757
758        return m_editItemId != null;
759    }
760
761    /**
762     * Returns if the given property is being edited.<p>
763     *
764     * @param propertyId the property id
765     *
766     * @return <code>true</code> if the given property is being edited
767     */
768    public boolean isEditProperty(CmsResourceTableProperty propertyId) {
769
770        return (m_editProperty != null) && m_editProperty.equals(propertyId);
771    }
772
773    /**
774     * Opens the context menu.<p>
775     *
776     * @param event the click event
777     */
778    public void openContextMenu(ItemClickEvent event) {
779
780        m_menu.openForTable(event, m_fileTable);
781    }
782
783    /**
784     * Removes the given cell style generator.<p>
785     *
786     * @param styleGenerator the cell style generator to remove
787     */
788    public void removeAdditionalStyleGenerator(Table.CellStyleGenerator styleGenerator) {
789
790        m_additionalStyleGenerators.remove(styleGenerator);
791    }
792
793    /**
794     * Restores container filters to the ones previously saved via saveFilters().
795     */
796    public void restoreFilters() {
797
798        IndexedContainer container = (IndexedContainer)m_fileTable.getContainerDataSource();
799        container.removeAllContainerFilters();
800        for (Filter filter : m_filters) {
801            container.addContainerFilter(filter);
802        }
803    }
804
805    /**
806     * Saves currently active filters.<p>
807     */
808    public void saveFilters() {
809
810        IndexedContainer container = (IndexedContainer)m_fileTable.getContainerDataSource();
811        m_filters = container.getContainerFilters();
812    }
813
814    /**
815     * Sets the default action column property.<p>
816     *
817     * @param actionColumnProperty the default action column property
818     */
819    public void setActionColumnProperty(CmsResourceTableProperty actionColumnProperty) {
820
821        m_actionColumnProperty = actionColumnProperty;
822    }
823
824    /**
825     * Sets the dialog context provider.<p>
826     *
827     * @param provider the dialog context provider
828     */
829    public void setContextProvider(I_CmsContextProvider provider) {
830
831        m_contextProvider = provider;
832    }
833
834    /**
835     * Sets the first visible item index.<p>
836     *
837     * @param i the item index
838     */
839    public void setFirstVisibleItemIndex(int i) {
840
841        m_fileTable.setCurrentPageFirstItemIndex(i);
842    }
843
844    /**
845     * Sets the folder select handler.<p>
846     *
847     * @param folderSelectHandler the folder select handler
848     */
849    public void setFolderSelectHandler(I_FolderSelectHandler folderSelectHandler) {
850
851        m_folderSelectHandler = folderSelectHandler;
852    }
853
854    /**
855     * Sets the menu builder.<p>
856     *
857     * @param builder the menu builder
858     */
859    public void setMenuBuilder(I_CmsContextMenuBuilder builder) {
860
861        m_menuBuilder = builder;
862    }
863
864    /**
865     * Sets the table state.<p>
866     *
867     * @param state the table state
868     */
869    public void setTableState(CmsFileExplorerSettings state) {
870
871        if (state != null) {
872            m_fileTable.setSortContainerPropertyId(state.getSortColumnId());
873            m_fileTable.setSortAscending(state.isSortAscending());
874            Object[] visibleCols = m_fileTable.getVisibleColumns();
875            for (int i = 0; i < visibleCols.length; i++) {
876                m_fileTable.setColumnCollapsed(visibleCols[i], state.getCollapsedColumns().contains(visibleCols[i]));
877            }
878        }
879    }
880
881    /**
882     * Starts inline editing of the given file property.<p>
883     *
884     * @param itemId the item resource structure id
885     * @param propertyId the property to edit
886     * @param editHandler the edit handler
887     */
888    public void startEdit(
889        CmsUUID itemId,
890        CmsResourceTableProperty propertyId,
891        I_CmsFilePropertyEditHandler editHandler) {
892
893        m_editItemId = itemId;
894        m_editProperty = propertyId;
895        m_originalEditValue = (String)m_container.getItem(m_editItemId.toString()).getItemProperty(
896            m_editProperty).getValue();
897        m_editHandler = editHandler;
898
899        // storing current drag mode and setting it to none to avoid text selection issues in IE11
900        m_beforEditDragMode = m_fileTable.getDragMode();
901        m_fileTable.setDragMode(TableDragMode.NONE);
902
903        m_fileTable.setEditable(true);
904    }
905
906    /**
907     * Stops the current edit process to save the changed property value.<p>
908     */
909    public void stopEdit() {
910
911        if (m_editHandler != null) {
912            String value = (String)m_container.getItem(m_editItemId.toString()).getItemProperty(
913                m_editProperty).getValue();
914            if (!value.equals(m_originalEditValue)) {
915                m_editHandler.validate(value);
916                m_editHandler.save(value);
917            } else {
918                // call cancel to ensure unlock
919                m_editHandler.cancel();
920            }
921        }
922        clearEdit();
923
924        // restoring drag mode
925        m_fileTable.setDragMode(m_beforEditDragMode);
926
927        m_beforEditDragMode = null;
928    }
929
930    /**
931     * Updates all items with ids from the given list.<p>
932     *
933     * @param ids the resource structure ids to update
934     * @param remove true if the item should be removed only
935     */
936    public void update(Collection<CmsUUID> ids, boolean remove) {
937
938        for (CmsUUID id : ids) {
939            updateItem(id, remove);
940        }
941        rebuildMenu();
942    }
943
944    /**
945     * Updates the column widths.<p>
946     *
947     * The reason this is needed is that the Vaadin table does not support minimum widths for columns,
948     * so expanding columns get squished when most of the horizontal space is used by other columns.
949     * So we try to determine whether the expanded columns would have enough space, and if not, give them a
950     * fixed width.
951     *
952     * @param estimatedSpace the estimated horizontal space available for the table.
953     */
954    public void updateColumnWidths(int estimatedSpace) {
955
956        Object[] cols = m_fileTable.getVisibleColumns();
957        List<CmsResourceTableProperty> expandCols = Lists.newArrayList();
958        int nonExpandWidth = 0;
959        int totalExpandMinWidth = 0;
960        for (Object colObj : cols) {
961            if (m_fileTable.isColumnCollapsed(colObj)) {
962                continue;
963            }
964            CmsResourceTableProperty prop = (CmsResourceTableProperty)colObj;
965            if (0 < m_fileTable.getColumnExpandRatio(prop)) {
966                expandCols.add(prop);
967                totalExpandMinWidth += getAlternativeWidthForExpandingColumns(prop);
968            } else {
969                nonExpandWidth += prop.getColumnWidth();
970            }
971        }
972        if (estimatedSpace < (totalExpandMinWidth + nonExpandWidth)) {
973            for (CmsResourceTableProperty expandCol : expandCols) {
974                m_fileTable.setColumnWidth(expandCol, getAlternativeWidthForExpandingColumns(expandCol));
975            }
976        }
977    }
978
979    /**
980     * Updates the file table sorting.<p>
981     */
982    public void updateSorting() {
983
984        m_fileTable.sort();
985    }
986
987    /**
988     * Cancels the current edit process.<p>
989     */
990    void cancelEdit() {
991
992        if (m_editHandler != null) {
993            m_editHandler.cancel();
994        }
995        clearEdit();
996    }
997
998    /**
999     * Returns the dialog context provider.<p>
1000     *
1001     * @return the dialog context provider
1002     */
1003    I_CmsContextProvider getContextProvider() {
1004
1005        return m_contextProvider;
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}