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}