/** * */ package org.javabuilders.swing.plugin.glazedlists.handler; import static org.javabuilders.swing.handler.type.TableColumnTypeHandler.EDITABLE; import static org.javabuilders.util.Preconditions.checkNotNull; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.JComponent; import javax.swing.JTable; import javax.swing.table.TableColumn; import javax.swing.text.JTextComponent; import org.javabuilders.BuildException; import org.javabuilders.BuildProcess; import org.javabuilders.Builder; import org.javabuilders.BuilderConfig; import org.javabuilders.Node; import org.javabuilders.handler.AbstractTypeHandler; import org.javabuilders.handler.ITypeChildrenHandler; import org.javabuilders.swing.handler.type.TableColumnTypeHandler; import org.javabuilders.swing.plugin.glazedlists.compiler.CompilerUtils; import org.javabuilders.util.BuilderUtils; import org.javabuilders.util.JBStringUtils; import org.javabuilders.util.PropertyUtils; import ca.odell.glazedlists.EventList; import ca.odell.glazedlists.FilterList; import ca.odell.glazedlists.SortedList; import ca.odell.glazedlists.TextFilterator; import ca.odell.glazedlists.gui.TableFormat; import ca.odell.glazedlists.impl.beans.BeanTableFormat; import ca.odell.glazedlists.matchers.MatcherEditor; import ca.odell.glazedlists.swing.EventTableModel; import ca.odell.glazedlists.swing.TableComparatorChooser; import ca.odell.glazedlists.swing.TextComponentMatcherEditor; /** * GlazedLists EventTableModel support * * @author Jacek Furmankiewicz * */ public class EventTableModelTypeHandler extends AbstractTypeHandler implements ITypeChildrenHandler { public static final String SOURCE = "source"; public static final String HEADER_VALUE = TableColumnTypeHandler.HEADER_VALUE; public static final String SORT = "sort"; public static final String SORT_SINGLE = "single"; public static final String SORT_MULTI = "multi"; public static final String SORT_BY = "sortBy"; public static final String COLUMNS = "columns"; private static final String TEXT_FILTERATOR = "TextFilterator"; private static final Pattern TEXT_FILTERATOR_REGEX = Pattern.compile("(?:[a-zA-Z]+\\()([a-zA-Z0-9]+)=\\[(.*)\\]"); public EventTableModelTypeHandler() { super(SOURCE, SORT,EDITABLE, COLUMNS, SORT_BY); } /* * (non-Javadoc) * * @see * org.javabuilders.handler.ITypeHandler#createNewInstance(org.javabuilders * .BuilderConfig, org.javabuilders.BuildProcess, org.javabuilders.Node, * java.lang.String, java.util.Map) */ @SuppressWarnings("unchecked") public Node createNewInstance(BuilderConfig config, BuildProcess process, Node parent, String key, final Map<String, Object> typeDefinition) throws BuildException { String source = (String) typeDefinition.get(SOURCE); checkNotNull(source, "EventTableModel.source property must be specified: {0}", typeDefinition); List<Map<String, Object>> cols = parent.getParent().getContentData(TableColumn.class); JTable table = (JTable) parent.getParentObject(JTable.class); Field field = BuilderUtils.getField(process.getCaller(), source, EventList.class); checkNotNull(field,"EventTableModel.source property does not point to a valid instance of GlazedLists EventList: {0}", typeDefinition); try { EventList list = GlazedListsUtils.getSource(process.getCaller(), typeDefinition).get0(); Class<?> type = BuilderUtils.getGenericsTypeFromCollectionField(field); if (type == null) { throw new BuildException("Unable to use generics to find type of object stored in source: {0}", source); } LinkedHashMap<String, String> columnNames = getColumnNamesAndHeaders(process, typeDefinition, cols, type); TableFormat tableFormat = createTableFormat(parent, type, typeDefinition, cols, columnNames); EventTableModel instance = setupModel(process, typeDefinition, table, list, cols, type,tableFormat, source); return useExistingInstance(config, process, parent, key, typeDefinition, instance); } catch (BuildException ex) { throw ex; } catch (Exception e) { throw new BuildException(e, "Unable to create instance of EventTableModel: {0}.\n{1}", typeDefinition,e.getMessage()); } } /* * (non-Javadoc) * * @see * org.javabuilders.handler.ITypeHandler#useExistingInstance(org.javabuilders * .BuilderConfig, org.javabuilders.BuildProcess, org.javabuilders.Node, * java.lang.String, java.util.Map, java.lang.Object) */ public Node useExistingInstance(BuilderConfig config, BuildProcess process, Node parent, String key, Map<String, Object> typeDefinition, Object instance) throws BuildException { Node node = new Node(parent, key, typeDefinition, instance); return node; } /* * (non-Javadoc) * * @see org.javabuilders.IApplicable#getApplicableClass() */ @SuppressWarnings("unchecked") public Class<EventTableModel> getApplicableClass() { return EventTableModel.class; } // creates the TableFormat class @SuppressWarnings({ "rawtypes", "unchecked" }) private TableFormat<?> createTableFormat(Node parent, Class<?> type, Map<String,Object> typeDefinition, List<Map<String, Object>> cols, final LinkedHashMap<String, String> columnNames) { final Integer[] editable = getEditableColumnIndexes(typeDefinition, cols); final String[] names = columnNames.keySet().toArray(new String[columnNames.size()]); final String[] headers = columnNames.values().toArray(new String[columnNames.size()]); final boolean[] ed = new boolean[names.length]; for(Integer i : editable) { ed[i] = true; } return new BeanTableFormat(type, names, headers, ed); } // looks at raw data to figure out which column it goes to @SuppressWarnings("unchecked") private LinkedHashMap<String, String> getColumnNamesAndHeaders(BuildProcess process, Map<String,Object> typeDefinition, List<Map<String, Object>> cols, Class<?> type) { LinkedHashMap<String, String> columns = new LinkedHashMap<String, String>(); final Set<String> props = PropertyUtils.getPropertyNames(type); //take initial list from what's defined in the columns=[] collection List<String> modelColumns = (List<String>) typeDefinition.get(COLUMNS); if (modelColumns != null) { for(String modelColumn : modelColumns) { columns.put(modelColumn, JBStringUtils.getDisplayLabel(process, type, modelColumn)); } } //add/override to it via whatever TableColumns have been defined if (cols.size() > 0) { for (Map<String, Object> map : cols) { String name = getColumnName(map); if (props.contains(name)) { String headerValue = (String) map.get(HEADER_VALUE); if (headerValue == null) { headerValue = JBStringUtils.getDisplayLabel(process, type, name); } // flag internally the column to map it to a particular // index of the model map.put(TableColumnTypeHandler.INTERNAL_MODEL_INDEX, columns.size()); columns.put(name, headerValue); } else { throw new BuildException("Unable to map column ''{0}'' to any property of type {1}", name, type); } } } //if no columns have been defined at all, just default to displaying all of them if (columns.size() == 0) { // columns not defined explicitly - show them all, sorted by name List<String> orderedProps = new ArrayList<String>(props); Collections.sort(orderedProps); for (String name : orderedProps) { columns.put(name, JBStringUtils.getDisplayLabel(process, type, name)); } } return columns; } // common logic to find the property name within the column definition private String getColumnName(Map<String, Object> map) { String name = null; if (map.containsKey(SOURCE)) { name = String.valueOf(map.get(SOURCE)); } else if (map.containsKey(Builder.NAME)) { name = String.valueOf(map.get(Builder.NAME)); } else { throw new BuildException( "TableColumn data does not contain 'source' or 'name' property. Unable to map it to the model: {0}", map); } return name; } // figures out if the actual source is the raw data, SortedList or a FilterList wrapper @SuppressWarnings({ "unchecked", "unused" }) private EventTableModel setupModel(BuildProcess process, Map<String, Object> typeDefinition, JTable table, EventList source, List<Map<String, Object>> cols, Class<?> type, TableFormat format,String sourceName) { EventList actualSource = source; SortedList sortedList = null; Object sortedChooser = null; EventList filterList = null; String sort = (String) typeDefinition.get(SORT); List<String> sortedColumns = (List<String>) typeDefinition.get(SORT_BY); // SORTING if (SORT_SINGLE.equals(sort) || SORT_MULTI.equals(sort)) { //attempt to use an existing sorted list, if defined (Issue # 127) String sortedListName = sourceName + "Sorted"; sortedList = (SortedList) BuilderUtils.getExistingInstanceIfAvailable(process.getCaller(), SortedList.class, process.getConfig(), sortedListName); if (sortedList == null) { //not found, we will create a default one sortedList = new SortedList(actualSource); process.getBuildResult().put(sortedListName, sortedList); } //handle sorted columns, if specified if (sortedColumns != null && sortedColumns.size() > 0) { Comparator c = CompilerUtils.newComparator(type, sortedColumns); sortedList.setComparator(c); } actualSource = sortedList; sortedChooser = (SORT_SINGLE.equals(sort)) ? TableComparatorChooser.SINGLE_COLUMN : TableComparatorChooser.MULTIPLE_COLUMN_MOUSE; } else if (sort != null) { throw new BuildException("Unknown value of EventTableModel.sort: {0}\n{1}", sort, typeDefinition); } //FILTERING Map<JComponent,List<String>> filterInfo = getModelFilters(process, typeDefinition); if (filterInfo.size() > 0) { for(JComponent filterField : filterInfo.keySet()) { List<String> filterColumns = filterInfo.get(filterField); if (filterField instanceof JTextComponent) { JTextComponent c = (JTextComponent) filterField; TextFilterator filterator = createTextFilterator(type, filterColumns); MatcherEditor textMatcherEditor = new TextComponentMatcherEditor(c, filterator); filterList = new FilterList<Object>(actualSource, textMatcherEditor); actualSource = filterList; } else { //throw new BuildException("EventTableModel.filterField does not point to a JTextComponent: {0}", // typeDefinition); } } } //put it all together, in the right order for GlazedLists to work EventTableModel model = new EventTableModel(actualSource, format); table.setModel(model); if (sortedList != null) { TableComparatorChooser tableSorter = TableComparatorChooser.install(table, (SortedList)sortedList, sortedChooser); } return model; } //uses Janino to compile an im-memory text filterator //we don't want to use Reflection for this due to performance overhead on filtering potentially thousands of rows @SuppressWarnings("unchecked") private TextFilterator<?> createTextFilterator(Class<?> type, List<String> columns) { StringBuilder bld = new StringBuilder(); String fullTypeName = type.getName(); Map<String,String> getters = new HashMap<String, String>(); Map<String,Class<?>> types = new HashMap<String, Class<?>>(); for(String column : columns) { String getter = "get" + column.substring(0,1).toUpperCase() + column.substring(1); getters.put(column, getter); try { Class<?> returnType = type.getMethod(getter).getReturnType(); types.put(column, returnType); } catch (Exception e) { throw new BuildException("Unable to get setter return value: {0}",e.getMessage()); } } String fullName = CompilerUtils.generateClassName(TextFilterator.class); bld.append("public void getFilterStrings(java.util.List baseList, Object target) {\n"); bld.append(" ").append(fullTypeName).append(" row = (").append(fullTypeName).append(")target;\n"); for(String column : columns) { if (types.get(column).equals(String.class)) { bld.append(" baseList.add(row.").append(getters.get(column)).append("());\n"); } else { //non-String type - wrap it bld.append(" baseList.add(String.valueOf(row.").append(getters.get(column)).append("());\n"); } } bld.append("}"); try { TextFilterator f = (TextFilterator) CompilerUtils.compile(fullName, bld.toString(),Object.class,TextFilterator.class).newInstance(); return f; } catch (Exception e) { throw new BuildException("Failed to compile TextFilterator for GlazedLists filtering: {0}\n{1}",e.getMessage(),bld.toString()); } } //find out which columns are editable private Integer[] getEditableColumnIndexes(Map<String, Object> typeDefinition, List<Map<String, Object>> cols) { Set<Integer> indexes = new LinkedHashSet<Integer>(); Boolean modelEditable = (Boolean) BuilderUtils.getValue(typeDefinition, EDITABLE, Boolean.FALSE); for(int i = 0; i < cols.size(); i++) { Map<String,Object> col = cols.get(i); Boolean colEditable = (Boolean) BuilderUtils.getValue(col, EDITABLE, modelEditable); if (colEditable) { indexes.add(i); } } return indexes.toArray(new Integer[indexes.size()]); } //scans the Model Filter element looking for filter info @SuppressWarnings("unchecked") private Map<JComponent,List<String>> getModelFilters(BuildProcess process, Map<String, Object> typeDefinition) { Map<JComponent,List<String>> map = new LinkedHashMap<JComponent, List<String>>(); ArrayList<String> content = (ArrayList<String>) typeDefinition.get(Builder.CONTENT); if (content != null && content.size() > 0) { for(String filter : content) { if (filter.startsWith(TEXT_FILTERATOR)) { Matcher m = TEXT_FILTERATOR_REGEX.matcher(filter); if (m.find()) { String field = m.group(1); String[] columns = m.group(2).split(","); JComponent c = (JComponent) process.getBuildResult().get(field); if (c != null) { List<String> cols = Arrays.asList(columns); if (cols.size() > 0) { map.put(c,cols); } else { throw new BuildException("TextFilterator field {0} needs to have at least 1 column", field); } } else { throw new BuildException("TextFilterator field {0} cannot be found", field); } } } } } return map; } }