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.util.table;
029
030import org.opencms.i18n.CmsMessages;
031import org.opencms.main.CmsLog;
032import org.opencms.ui.CmsVaadinUtils;
033import org.opencms.util.CmsMacroResolver;
034import org.opencms.util.CmsStringUtil;
035
036import java.beans.IntrospectionException;
037import java.beans.PropertyDescriptor;
038import java.lang.reflect.Method;
039import java.util.Collections;
040import java.util.Comparator;
041import java.util.List;
042
043import org.apache.commons.logging.Log;
044
045import com.google.common.collect.ComparisonChain;
046import com.google.common.collect.Lists;
047import com.vaadin.data.util.BeanUtil;
048import com.vaadin.ui.Button;
049import com.vaadin.v7.data.Container.Filter;
050import com.vaadin.v7.data.Item;
051import com.vaadin.v7.data.util.BeanItemContainer;
052import com.vaadin.v7.ui.Table;
053import com.vaadin.v7.ui.Table.Align;
054import com.vaadin.v7.ui.Table.CellStyleGenerator;
055
056/**
057 * Builds a table based on a given bean class.<p>
058 *
059 * The columns of the table correspond to getters of the given bean class with the Column annotation.
060 *
061 * @param <T> The class of the bean containing the metadata of the table
062 */
063public class CmsBeanTableBuilder<T> {
064
065    /**
066     * Contains information about a single column.<p>
067     */
068    private class ColumnBean {
069
070        /** The annotation for the getter. */
071        private Column m_info;
072
073        /** Property descriptor for the getter for that column. */
074        private PropertyDescriptor m_property;
075
076        /**
077         * Creates a new instance.<p>
078         *
079         * @param property the property descriptor for the getter
080         * @param info the annotation for the getter
081         */
082        public ColumnBean(PropertyDescriptor property, Column info) {
083
084            super();
085            m_property = property;
086            m_info = info;
087        }
088
089        /**
090         * Returns the info.<p>
091         *
092         * @return the info
093         */
094        public Column getInfo() {
095
096            return m_info;
097        }
098
099        /**
100         * Returns the property.<p>
101         *
102         * @return the property
103         */
104        public PropertyDescriptor getProperty() {
105
106            return m_property;
107        }
108    }
109
110    /** Logger instance for this class. */
111    private static final Log LOG = CmsLog.getLog(CmsBeanTableBuilder.class);
112
113    /** Bean type for the table. */
114    private Class<T> m_class;
115
116    /** Beans representing the table columns. */
117    private List<ColumnBean> m_columns = Lists.newArrayList();
118
119    /** The macro resolver to use for resolving macros in column headers. */
120    private CmsMacroResolver m_macroResolver = new CmsMacroResolver();
121
122    /** The current view. */
123    private String m_view;
124
125    /**
126     * Creates a new table builder instance for the given bean class and view.<p>
127     *
128     * Depending on the view configuration of the columns, columns may be hidden depending on the view.
129     *
130     * @param cls the bean class
131     * @param view the selected view
132     *
133     */
134    public CmsBeanTableBuilder(Class<T> cls, String view) {
135
136        m_class = cls;
137        m_view = view;
138        try {
139            List<PropertyDescriptor> descriptors = BeanUtil.getBeanPropertyDescriptors(m_class);
140            for (PropertyDescriptor desc : descriptors) {
141                Method getter = desc.getReadMethod();
142                if (getter != null) {
143                    Column columnInfo = getter.getAnnotation(Column.class);
144                    if (columnInfo != null) {
145                        if ((columnInfo.view() == null) || matchView(m_view, columnInfo.view())) {
146                            m_columns.add(new ColumnBean(desc, columnInfo));
147                        }
148                    }
149                }
150            }
151
152            Collections.sort(m_columns, new Comparator<ColumnBean>() {
153
154                public int compare(CmsBeanTableBuilder<T>.ColumnBean col1, CmsBeanTableBuilder<T>.ColumnBean col2) {
155
156                    return ComparisonChain.start().compare(col1.getInfo().order(), col2.getInfo().order()).result();
157                }
158            });
159        } catch (IntrospectionException e) {
160            // Shouldn't normally happen
161            LOG.error(e.getLocalizedMessage(), e);
162            throw new IllegalArgumentException(e);
163
164        }
165    }
166
167    /**
168     * Checks if the given string is likely a message key.<p>
169     *
170     * @param str the input string
171     * @return true if this is probably a message key
172     */
173    public static boolean isProbablyMessageKey(String str) {
174
175        return str.matches("^[A-Z]+_[A-Z0-9_]*$");
176    }
177
178    /**
179     * Convenience method used to create a new instance of a table builder.<p>
180     *
181     * @param cls the bean class
182     * @return the new table builder
183     */
184    public static <V> CmsBeanTableBuilder<V> newInstance(Class<V> cls) {
185
186        return new CmsBeanTableBuilder<V>(cls, null);
187
188    }
189
190    /**
191     * Convenience method used to create a new instance of a table builder.<p>
192     *
193     * @param cls the bean class
194     * @param view the selected view
195     *
196     * @return the new table builder
197     */
198    public static <V> CmsBeanTableBuilder<V> newInstance(Class<V> cls, String view) {
199
200        return new CmsBeanTableBuilder<V>(cls, view);
201
202    }
203
204    /**
205     * Builds a table and uses the given beans to fill its rows.<p>
206     *
207     * @param beans the beans to display in the table
208     *
209     * @return the finished table
210     */
211    public Table buildTable(List<T> beans) {
212
213        Table table = new Table();
214        buildTable(table, beans);
215        return table;
216    }
217
218    /**
219     * Sets up a table and uses the given beans to fill its rows, but does not actually create the table instance; it uses the passed in table instance instead.<p>
220     *
221     * @param table the table to set up
222     * @param beans the beans to display in the table
223     *
224     */
225    public void buildTable(Table table, List<T> beans) {
226
227        BeanItemContainer<T> container = new BeanItemContainer<T>(m_class);
228        List<String> visibleCols = Lists.newArrayList();
229        for (ColumnBean column : m_columns) {
230            String propName = column.getProperty().getName();
231            String columnHeader = column.getInfo().header();
232            String localizedHeader = CmsVaadinUtils.getMessageText(columnHeader);
233            if (CmsMessages.isUnknownKey(localizedHeader)) {
234                localizedHeader = columnHeader;
235            }
236            localizedHeader = m_macroResolver.resolveMacros(localizedHeader);
237            table.setColumnHeader(propName, localizedHeader);
238            if (Button.class.isAssignableFrom(column.getProperty().getPropertyType())) {
239                table.setColumnAlignment(propName, Align.CENTER);
240            }
241            visibleCols.add(propName);
242        }
243        table.setContainerDataSource(container);
244        table.setVisibleColumns(visibleCols.toArray());
245
246        for (ColumnBean column : m_columns) {
247            Column info = column.getInfo();
248            String name = column.getProperty().getName();
249            if (info.width() >= 0) {
250                table.setColumnWidth(name, info.width());
251            }
252            if (info.expandRatio() >= 0) {
253                table.setColumnExpandRatio(name, info.expandRatio());
254            }
255        }
256        for (T bean : beans) {
257            container.addBean(bean);
258        }
259
260    }
261
262    /**
263     * Creates a default cell style generator which just returns the value of the styleName attribute in a Column annotation for cells in that column.<p>
264     *
265     * @return the default cell style generator
266     */
267    public CellStyleGenerator getDefaultCellStyleGenerator() {
268
269        return new CellStyleGenerator() {
270
271            private static final long serialVersionUID = 1L;
272
273            @SuppressWarnings("synthetic-access")
274            public String getStyle(Table source, Object itemId, Object propertyId) {
275
276                for (ColumnBean colBean : m_columns) {
277                    if (colBean.getProperty().getName().equals(propertyId)) {
278                        return colBean.getInfo().styleName();
279                    }
280                }
281                return "";
282            }
283        };
284    }
285
286    /**
287     * Creates a default filter which just searches the lower case version of the result of the toString() method applied to all columns with the annotation attribute filterable = true.<P>
288     *
289     * @param filterString the string for which to filter
290     *
291     * @return the default filter for the given filter string
292     */
293    public Filter getDefaultFilter(final String filterString) {
294
295        return new Filter() {
296
297            private static final long serialVersionUID = 1L;
298
299            @SuppressWarnings("synthetic-access")
300            public boolean appliesToProperty(Object propertyId) {
301
302                for (ColumnBean col : m_columns) {
303                    if (col.getProperty().getName().equals(propertyId) && col.getInfo().filterable()) {
304                        return true;
305                    }
306                }
307                return false;
308            }
309
310            @SuppressWarnings("synthetic-access")
311            public boolean passesFilter(Object itemId, Item item) throws UnsupportedOperationException {
312
313                if (CmsStringUtil.isEmpty(filterString)) {
314                    return true;
315                }
316                T bean = (T)itemId;
317                for (ColumnBean col : m_columns) {
318                    if (col.getInfo().filterable()) {
319                        if (("" + item.getItemProperty(col.getProperty().getName()).getValue()).toLowerCase().contains(
320                            filterString)) {
321                            return true;
322                        }
323                    }
324                }
325                return false;
326            }
327        };
328
329    }
330
331    /**
332     * Gets the macro resolver which is used for column headers.<p>
333     *
334     * @return the macro resolver for column headers
335     */
336    public CmsMacroResolver getMacroResolver() {
337
338        return m_macroResolver;
339    }
340
341    /**
342     * Sets the macro resolver.<p>
343     *
344     * @param resolver the macro resolver
345     */
346    public void setMacroResolver(CmsMacroResolver resolver) {
347
348        m_macroResolver = resolver;
349    }
350
351    /**
352     * Checks if the actual view matches a view declaration.<p>
353     *
354     * @param actualView the actual view
355     * @param declaredView the declared view string
356     *
357     * @return true if the view matches
358     */
359    private boolean matchView(String actualView, String declaredView) {
360
361        if (CmsStringUtil.isEmptyOrWhitespaceOnly(declaredView) || CmsStringUtil.isEmptyOrWhitespaceOnly(actualView)) {
362            return true;
363        }
364        return CmsStringUtil.splitAsList(declaredView, "|").contains(actualView);
365
366    }
367}