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