001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://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: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://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.dataview;
029
030import org.opencms.ui.CmsVaadinUtils;
031import org.opencms.ui.components.OpenCmsTheme;
032import org.opencms.ui.dataview.CmsPagingControls.I_PagingCallback;
033import org.opencms.ui.util.CmsComponentField;
034import org.opencms.widgets.dataview.CmsDataViewColumn;
035import org.opencms.widgets.dataview.CmsDataViewFilter;
036import org.opencms.widgets.dataview.CmsDataViewQuery;
037import org.opencms.widgets.dataview.CmsDataViewResult;
038import org.opencms.widgets.dataview.I_CmsDataView;
039import org.opencms.widgets.dataview.I_CmsDataViewItem;
040
041import java.util.ArrayList;
042import java.util.Collection;
043import java.util.LinkedHashMap;
044import java.util.List;
045import java.util.Map;
046import java.util.Set;
047
048import com.google.common.collect.Lists;
049import com.google.common.collect.Maps;
050import com.google.common.collect.Sets;
051import com.vaadin.event.ShortcutAction.KeyCode;
052import com.vaadin.event.ShortcutListener;
053import com.vaadin.ui.Button;
054import com.vaadin.ui.Button.ClickEvent;
055import com.vaadin.ui.Button.ClickListener;
056import com.vaadin.ui.CssLayout;
057import com.vaadin.v7.data.Item;
058import com.vaadin.v7.data.Property.ValueChangeEvent;
059import com.vaadin.v7.data.Property.ValueChangeListener;
060import com.vaadin.v7.data.util.IndexedContainer;
061import com.vaadin.v7.ui.CheckBox;
062import com.vaadin.v7.ui.ComboBox;
063import com.vaadin.v7.ui.HorizontalLayout;
064import com.vaadin.v7.ui.Table;
065import com.vaadin.v7.ui.Table.ColumnGenerator;
066import com.vaadin.v7.ui.TextField;
067import com.vaadin.v7.ui.VerticalLayout;
068
069/**
070 * Panel containing both the interface elements used to search the data source (query field, filter select boxes) as well
071 * as the paged list of search results.<p>
072 */
073public class CmsDataViewPanel extends VerticalLayout {
074
075    /**
076     * Subclass of Table, which we need because we want to trigger a complete refresh when sorting instead of just sorting the in-memory data.<p>
077     */
078    public class PagedTable extends Table {
079
080        /** Serial version id. */
081        private static final long serialVersionUID = 1L;
082
083        /**
084         * Creates a new instance.<p>
085         *
086         * @param container the data container
087         */
088        public PagedTable(IndexedContainer container) {
089
090            super();
091            setContainerDataSource(container);
092        }
093
094        /**
095         * @see com.vaadin.v7.ui.Table#sort(java.lang.Object[], boolean[])
096         */
097        @Override
098        public void sort(Object[] propertyId, boolean[] ascending) throws UnsupportedOperationException {
099
100            m_sortCol = propertyId[0];
101            m_ascending = ascending[0];
102            refreshData(true, null);
103        }
104    }
105
106    /** Serial version id. */
107    private static final long serialVersionUID = 1L;
108
109    /** The ID column name. */
110    public static final Object ID_COLUMN = new Object();
111
112    /** The data view instance used to access the data. */
113    private I_CmsDataView m_dataView;
114
115    /** Search button. */
116    private Button m_searchButton;
117
118    /** The container used to store the current page of search results. */
119    private IndexedContainer m_container;
120
121    /** Widget used to move to different pages. */
122    private CmsPagingControls m_pagingControls;
123
124    /** The table container. */
125    private CssLayout m_tablePlaceholder;
126
127    /** The widget containing the filters. */
128    private HorizontalLayout m_filterContainer;
129
130    /** Query text field. */
131    private TextField m_fullTextSearch;
132
133    /** The sort column. */
134    protected Object m_sortCol;
135
136    /** The sort direction. */
137    protected boolean m_ascending;
138
139    /** The current list of filters. */
140    private List<CmsDataViewFilter> m_filters = Lists.newArrayList();
141
142    /** Map of check boxes. */
143    private Map<Object, CheckBox> m_checkBoxes = Maps.newHashMap();
144
145    /** The current map of filters, by id. */
146    private Map<String, CmsDataViewFilter> m_filterMap = Maps.newLinkedHashMap();
147
148    /** The table with the search results. */
149    private CmsComponentField<Table> m_table = CmsComponentField.newInstance();
150
151    /** True if we are currently in a recursive call of the value change event listener. */
152    private boolean m_recursiveValueChange;
153
154    /** The real selection (includes item. */
155    private Set<Object> m_realSelection = Sets.newHashSet();
156
157    /**
158    * Creates a new instance.<p>
159    *
160    * @param viewInstance the data view instance
161    * @param multiselect true if multi-selection should be allowed
162    */
163    public CmsDataViewPanel(I_CmsDataView viewInstance, boolean multiselect) {
164
165        m_dataView = viewInstance;
166        CmsVaadinUtils.readAndLocalizeDesign(this, CmsVaadinUtils.getWpMessagesForCurrentLocale(), null);
167
168        m_pagingControls.addCallback(new I_PagingCallback() {
169
170            public void pageChanged(int page) {
171
172                refreshData(false, null);
173            }
174        });
175        m_fullTextSearch.addShortcutListener(new ShortcutListener("Save", KeyCode.ENTER, null) {
176
177            private static final long serialVersionUID = 1L;
178
179            @Override
180            public void handleAction(Object sender, Object target) {
181
182                refreshData(false, null);
183
184            }
185        });
186        m_searchButton.addClickListener(new ClickListener() {
187
188            private static final long serialVersionUID = 1L;
189
190            public void buttonClick(ClickEvent event) {
191
192                refreshData(true, null);
193            }
194
195        });
196        m_container = new IndexedContainer();
197        for (CmsDataViewColumn column : m_dataView.getColumns()) {
198            m_container.addContainerProperty(
199                column.getId(),
200                CmsColumnValueConverter.getColumnClass(column.getType()),
201                null);
202
203        }
204        m_container.addContainerProperty(ID_COLUMN, String.class, null);
205
206        final PagedTable table = new PagedTable(m_container);
207        table.addStyleName(OpenCmsTheme.TABLE_CELL_PADDING);
208        table.setMultiSelect(multiselect);
209
210        table.addGeneratedColumn("checked", new ColumnGenerator() {
211
212            private static final long serialVersionUID = 1L;
213
214            @SuppressWarnings("synthetic-access")
215            public Object generateCell(final Table source, final Object itemId, final Object columnId) {
216
217                CheckBox cb = getCheckBox(itemId);
218                cb.setValue(Boolean.valueOf(source.isSelected(itemId)));
219                cb.addValueChangeListener(new ValueChangeListener() {
220
221                    private static final long serialVersionUID = 1L;
222
223                    public void valueChange(ValueChangeEvent event) {
224
225                        boolean val = ((Boolean)(event.getProperty().getValue())).booleanValue();
226                        if (val) {
227                            source.select(itemId);
228                        } else {
229                            source.unselect(itemId);
230                        }
231                    }
232                });
233                return cb;
234            }
235        });
236
237        table.addStyleName("o-wrap-table");
238        Object[] visibleCols = new String[m_dataView.getColumns().size() + 1];
239        visibleCols[0] = "checked";
240        int i = 1;
241        for (CmsDataViewColumn col : m_dataView.getColumns()) {
242            visibleCols[i++] = col.getId();
243
244        }
245        table.setVisibleColumns(visibleCols);
246        table.setColumnWidth("checked", 45);
247        table.setColumnHeader("checked", "");
248        for (CmsDataViewColumn col : m_dataView.getColumns()) {
249            table.setColumnHeader(col.getId(), col.getNiceName());
250            table.setColumnWidth(col.getId(), col.getPreferredWidth());
251        }
252
253        table.setPageLength(0);
254        table.setWidth("100%"); //
255        table.setHeight("100%");
256        m_table.set(table);
257        table.setSelectable(true);
258        replaceComponent(m_tablePlaceholder, table);
259        setExpandRatio(table, 1.0f);
260
261        addAttachListener(new AttachListener() {
262
263            private static final long serialVersionUID = 1L;
264
265            public void attach(AttachEvent event) {
266
267                refreshData(true, null);
268            }
269        });
270        table.addValueChangeListener(new ValueChangeListener() {
271
272            private static final long serialVersionUID = 1L;
273
274            @SuppressWarnings("synthetic-access")
275            public void valueChange(ValueChangeEvent event) {
276
277                if (table.isMultiSelect()) {
278                    if (m_recursiveValueChange) {
279                        updateCheckboxesWithSelectedIds(m_realSelection);
280                    } else {
281                        updateRealSelection(getIdsFromSelection(event));
282                        if (m_realSelection.equals(event.getProperty().getValue())) {
283                            updateCheckboxesWithSelectedIds(m_realSelection);
284                        } else {
285                            try {
286                                m_recursiveValueChange = true;
287                                m_table.get().setValue(m_realSelection);
288                            } finally {
289                                m_recursiveValueChange = false;
290                            }
291                        }
292                    }
293
294                } else {
295                    Set<Object> ids = getIdsFromSelection(event);
296                    updateCheckboxesWithSelectedIds(ids);
297
298                }
299
300            }
301
302            /**
303             * Gets the ids from the selection event.<p>
304             *
305             * @param event a selection event
306             *
307             * @return the set of ids from the selection event
308             */
309            protected Set<Object> getIdsFromSelection(ValueChangeEvent event) {
310
311                Set<Object> ids = Sets.newHashSet();
312                if (event != null) {
313                    if (event.getProperty().getValue() instanceof Collection) {
314                        ids.addAll((Collection<?>)event.getProperty().getValue());
315                    } else {
316                        ids.add(event.getProperty().getValue());
317                    }
318                }
319                return ids;
320            }
321
322            @SuppressWarnings("synthetic-access")
323            protected void updateCheckboxesWithSelectedIds(Set<Object> selectedIds) {
324
325                for (Map.Entry<Object, CheckBox> entry : m_checkBoxes.entrySet()) {
326                    if (!(selectedIds.contains(entry.getKey()))) {
327                        entry.getValue().setValue(Boolean.FALSE);
328                    }
329                }
330
331                for (Object id : selectedIds) {
332                    getCheckBox(id).setValue(Boolean.TRUE);
333                }
334            }
335        });
336        List<CmsDataViewFilter> filters = new ArrayList<CmsDataViewFilter>(m_dataView.getFilters());
337        updateFilters(filters);
338    }
339
340    /**
341     * Fills the given item.<p>
342     *
343     * @param item the data view item
344     * @param target the table item
345     */
346    public void fillItem(I_CmsDataViewItem item, Item target) {
347
348        for (CmsDataViewColumn column : m_dataView.getColumns()) {
349            String name = column.getId();
350            Object value = CmsColumnValueConverter.getColumnValue(item.getColumnData(name), column.getType());
351            target.getItemProperty(name).setValue(value);
352        }
353        target.getItemProperty(CmsDataViewPanel.ID_COLUMN).setValue(item.getId());
354    }
355
356    /**
357     * Gets the list of selected data items.<p>
358     *
359     * If this widget is not in multi-select mode, a list with a single result will be returned.<p>
360     *
361     * @return the selected results
362     */
363    public List<I_CmsDataViewItem> getSelection() {
364
365        List<I_CmsDataViewItem> result = Lists.newArrayList();
366        Object val = m_table.get().getValue();
367        if (val == null) {
368            return result;
369        }
370        if (val instanceof Collection) {
371            Collection<?> results = (Collection<?>)val;
372            for (Object obj : results) {
373                result.add(m_dataView.getItemById((String)obj));
374            }
375        } else {
376            result.add(m_dataView.getItemById((String)val));
377        }
378        return result;
379
380    }
381
382    /**
383     * Gets the table.<p>
384     *
385     * @return the table
386     */
387    public Table getTable() {
388
389        return m_table.get();
390    }
391
392    /**
393     * Updates the data displayed in the table.<p>
394     *
395     * @param resetPaging true if we should go back to page 1
396     * @param textQuery the text query to use
397     */
398    public void refreshData(boolean resetPaging, String textQuery) {
399
400        String fullTextQuery = textQuery != null ? textQuery : m_fullTextSearch.getValue();
401        LinkedHashMap<String, String> filterValues = new LinkedHashMap<String, String>();
402        for (Map.Entry<String, CmsDataViewFilter> entry : m_filterMap.entrySet()) {
403            filterValues.put(entry.getKey(), entry.getValue().getValue());
404
405        }
406        CmsDataViewQuery query = new CmsDataViewQuery();
407        String sortCol = (String)m_sortCol;
408        boolean ascending = m_ascending;
409        query.setFullTextQuery(fullTextQuery);
410        query.setFilterValues(filterValues);
411        query.setSortColumn(sortCol);
412        query.setSortAscending(ascending);
413        CmsDataViewResult result = m_dataView.getResults(
414            query,
415            resetPaging ? 0 : getOffset(),
416            m_dataView.getPageSize());
417        m_container.removeAllItems();
418        for (I_CmsDataViewItem item : result.getItems()) {
419            fillItem(item, m_container.addItem(item.getId()));
420        }
421        //m_tablePanel.setScrollTop(0);
422
423        if (resetPaging) {
424            int total = result.getHitCount();
425            m_pagingControls.reset(result.getHitCount(), m_dataView.getPageSize(), false);
426        }
427    }
428
429    /**
430     * Updates the search results after a filter is changed by the user.<p>
431     *
432     * @param id the filter id
433     * @param value the filter value
434     */
435    public void updateFilter(String id, String value) {
436
437        CmsDataViewFilter oldFilter = m_filterMap.get(id);
438        CmsDataViewFilter newFilter = oldFilter.copyWithValue(value);
439        m_filterMap.put(id, newFilter);
440        List<CmsDataViewFilter> filters = new ArrayList<CmsDataViewFilter>(m_filterMap.values());
441        updateFilters(m_dataView.updateFilters(filters));
442    }
443
444    /**
445     * Changes the displayed filters to a new set.<p>
446     *
447     * @param newFilters the new filters
448     */
449    public void updateFilters(List<CmsDataViewFilter> newFilters) {
450
451        if (newFilters.isEmpty()) {
452            m_filterContainer.setVisible(false);
453        }
454        if (m_filters.equals(newFilters)) {
455            return;
456        }
457        m_filterContainer.removeAllComponents();
458        m_filters = newFilters;
459        m_filterMap.clear();
460        for (CmsDataViewFilter filter : newFilters) {
461            m_filterMap.put(filter.getId(), filter);
462            final CmsDataViewFilter finalFilter = filter;
463            ComboBox select = new ComboBox(filter.getNiceName());
464            select.setWidth("175px");
465            select.setNullSelectionAllowed(false);
466            select.setPageLength(0);
467            Map<String, String> options = filter.getOptions();
468            for (Map.Entry<String, String> entry : options.entrySet()) {
469                String key = entry.getKey();
470                String value = entry.getValue();
471                select.addItem(key);
472                select.setItemCaption(key, value);
473            }
474            select.setValue(filter.getValue());
475            if (filter.getHelpText() != null) {
476                select.setDescription(filter.getHelpText());
477            }
478
479            select.addValueChangeListener(new ValueChangeListener() {
480
481                private static final long serialVersionUID = 1L;
482
483                public void valueChange(ValueChangeEvent event) {
484
485                    String newValue = (String)(event.getProperty().getValue());
486                    updateFilter(finalFilter.getId(), newValue);
487                }
488            });
489            m_filterContainer.addComponent(select);
490        }
491    }
492
493    /**
494     * Updates the real selection, given the item ids from the selection event.<p>
495     *
496     * @param selectionEventIds the item ids from the selection event
497     */
498    protected void updateRealSelection(Set<Object> selectionEventIds) {
499
500        Set<Object> pageItems = Sets.newHashSet(m_table.get().getContainerDataSource().getItemIds());
501        Set<Object> result = Sets.newHashSet(m_realSelection);
502        result.removeAll(pageItems);
503        result.addAll(selectionEventIds);
504        m_realSelection = result;
505
506    }
507
508    /**
509     * Gets the check box for the item with the given id.<p>
510     *
511     * @param id the item id
512     * @return the check box
513     */
514    private CheckBox getCheckBox(Object id) {
515
516        if (!m_checkBoxes.containsKey(id)) {
517            m_checkBoxes.put(id, new CheckBox());
518        }
519        return m_checkBoxes.get(id);
520    }
521
522    /**
523     * Gets the offset.<p>
524     *
525     * @return the offset
526     */
527    private int getOffset() {
528
529        return m_pagingControls.getPage() * m_dataView.getPageSize();
530    }
531
532}