Dynamic SQL and Vaadin Table Component

To all Gurus!

I am trying to create a simple application where I need to present data from unknown but a valid SQL SELECT statement. There is no need to have any INSERT/UPDATE ability.

In the book of Vaadin, there is a quick reference that it is possible to bind a data source to table but I couldn’t find such example. There are several examples of using container with static data and data types initialization but that couldn’t apply in my specific scenario. I am primarily a DBA and I have just enough of Java. What I do not have is a lot of time on hand so my second degree java skill finally help me with this.

Could someone please point to such example or fabricate one, if possible!

Thanks!
DP

Hi,

I can think of several ways to do this:

  • One way would be to do the SQL query, fetch the results and populate a Table (IndexedContainer) with the results. E.g. in pseudo code:

results = fetchQueryResult();
table.addContainerProperty("First column",String.class,null);
[...]


foreach (result : results) {
	Item item = table.addItem(<some id for the row>);
	item.getProperty("First column").setValue(<value from result>);
	[...]

}
  • Another way would be to populate data objects with the result of the query and use BeanItemContainer. E.g.

List<DataObject> objects = executeQuery();
BeanItemContainer<DataObject> container = new BeanItemContainer<DataObject>(DataObject.class)
for (DataObject o : objects) {
    container.addBean(o);
}
table.setContainerDataSource(container);
  • A third way would be to use the HbnContainer available from the incubator. This container supports automatic fetching of results from a database using a Hibernate query.

Hi,

Thanks - this is a good lead.

Here are my questions though, and it’s largely because of my own short coming and limited exposure to vaadin.

In the example 1, since we are providing columns type and heading which must be known upfront, doesn’t that limit the SQL that could be run? Since my requirements are based on dynamic SQL, I am not sure of no of columns and types of data the query might fetch.

I could see the example 2 might help but again the same question applies to it as well, since a BeanItemContainer must be able to hold “item” i.e. a “row” from the query and it means query is fixed and not dynamic.

Could you please explain my doubts and if they are valid, does it mean we really could not have vaadin table to adopt a resultset dynamically? Is there anyway out?

Thanks for your help!
-DP.

I think what I am asking here is addressed by post titled “querycontainer statement lifecycle”.
I will look into this further.

-DP

Can someone please help me with this?

Please Please - simply don’t comment on this. I would love to see a working example of this. Could someone modify this code and show me exactly how a Table component is supposed to work with QueryContainer? Since Table component has constructor based on Container, Is there any other need than simply be able to pass a valid container and leave it upto Table to fetch data from it?

I am getting frustrated and I don’t wont to give up on vaadin - I think it’s best out of all frameworks out there, and it should be capable for this task.

Here is my simple attempt to use Table and QueryContainer. I am not getting anything except gray table borders!



package com.example.testemp;

import java.sql.Connection;
import java.sql.DriverManager;
import java.util.Iterator;
import java.util.Vector;

import com.vaadin.Application;
import com.vaadin.data.util.IndexedContainer;
import com.vaadin.data.util.QueryContainer;
import com.vaadin.ui.*;

public class TestempApplication extends Application {
		
		private Table tblDataTable = null;
	    private QueryContainer qcSQL = null;
		private Connection conn;
	
        @Override 
        public void init() {
        		
        		try {
                    
                 DriverManager.registerDriver(new oracle.jdbc.driver.OracleDriver());
                 conn = DriverManager.getConnection("jdbc:oracle:thin:@r1.mycomm.com:1526:devdb","scott","tiger");

                	tblDataTable = new Table();                    
    				qcSQL = new QueryContainer("SELECT * FROM EMP",conn);
    				IndexedContainer ic = new IndexedContainer(qcSQL.getItemIds());
    				qcSQL.close();
    				tblDataTable.setContainerDataSource(ic);
    				tblDataTable.setVisible(true);    				
                    
        		}
        		catch (Exception e) {
        			e.printStackTrace();
        		}

                Window mainWindow = new Window("Testemp Application");
                Label label = new Label("Hello Vaadin user");
                mainWindow.addComponent(label);
                mainWindow.addComponent(tblDataTable);
                setMainWindow(mainWindow);
        }

}

-Thanks!
DP

Hi!

Your code looks mostly ok, except for this snippet here:

When you create a new IndexedContainer with only the itemId:s that you get from the QueryContainer, you actually throw away all the information that the QueryContainer has loaded from the database. A fresh IndexedContainer has no properties, and thus nothing is shown in the Table. The easiest way is to simply assign the QueryContainer to the Table, changing your code as follows:

    				qcSQL = new QueryContainer("SELECT * FROM EMP",conn);
    				tblDataTable.setContainerDataSource(qcSQL);

This should show all the columns in your EMP table. You can hide certain columns using the Table.setVisibleColumns() method.

I know that this isn’t a full working code example, but changing your code like this should work – tell me if it doesn’t :wink:

HTH,
/Jonatan

I got this solved with an ItemSetChangeLIstener implementation:


public class DefaultTableSetupListener implements Container.ItemSetChangeListener {
    private PropertyFilter showColumnFilter = PropertyFilter.ByPattern.createIdFilter();
    private PropertyFilter sortColumnFilter = PropertyFilter.ByPattern.createIdFilter();
    private Transformer headerTransform = new Transformer.Header();
    private Table rowMapTable;
    private LinkedHashMap<Object,Table.ColumnGenerator> columnGenerators = new LinkedHashMap<Object,Table.ColumnGenerator>();

    public DefaultTableSetupListener(RowMapTable rowMapTable) {
        this.rowMapTable = rowMapTable;
    }

    @Override
    public void containerItemSetChange(Container.ItemSetChangeEvent event) {
        log.info("Container.ItemSetChangeEvent");
        processColumns();
    }

    private void processColumns() {

        ArrayList<Object> visibleCols = new ArrayList<Object>();

        // only process columns if container has items
        if(rowMapTable.getContainerDataSource().size()==0) return;

        for(Map.Entry<Object,Table.ColumnGenerator> eg:columnGenerators.entrySet()){
            rowMapTable.removeGeneratedColumn(eg.getKey());
            rowMapTable.addGeneratedColumn(eg.getKey(),eg.getValue());
            visibleCols.add(eg.getKey());
        }

        Collection c = rowMapTable.getContainerPropertyIds();
        Collection s = rowMapTable.getSortableContainerPropertyIds();
        for (Object l : c) {
            try {
                visibleCols.add(l);
                rowMapTable.setColumnHeader(l, headerTransform.transform(l).toString());
                if(rowMapTable.isColumnCollapsingAllowed()) {
                    rowMapTable.setColumnCollapsed(l, !showColumnFilter.accepts(l));
                }
                if (sortColumnFilter.accepts(l)) {
                    s.add(l);
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        rowMapTable.setVisibleColumns(visibleCols.toArray());
    }


    public PropertyFilter getShowColumnFilter() {
        return showColumnFilter;
    }

    public void setShowColumnFilter(PropertyFilter showColumnFilter) {
        this.showColumnFilter = showColumnFilter;
    }

    public PropertyFilter getSortColumnFilter() {
        return sortColumnFilter;
    }

    public void setSortColumnFilter(PropertyFilter sortColumnFilter) {

        this.sortColumnFilter = sortColumnFilter;
    }

    public Transformer getHeaderTransform() {
        return headerTransform;
    }

    public void setHeaderTransform(Transformer headerTransform) {
        this.headerTransform = headerTransform;
    }


    public LinkedHashMap<Object, Table.ColumnGenerator> getColumnGenerators() {
        return columnGenerators;
    }

    public void setColumnGenerators(LinkedHashMap<Object, Table.ColumnGenerator> columnGenerators) {
        this.columnGenerators = columnGenerators;
    }

    protected Logger log = Logger.getLogger(getClass());
}

Property Filter Interface…


public interface PropertyFilter {
    boolean accepts(Object propertyId);
}

Transformer Interface to tweak column headings


public interface Transformer {
    Object transform(Object in);
}

My Container implementation (This would be editable if backed by a JDBC RowSet )


import com.vaadin.data.Container;
import com.vaadin.data.Item;
import com.vaadin.data.Property;
import org.apache.commons.lang.NotImplementedException;
import org.apache.log4j.Logger;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.UncategorizedSQLException;
import org.springframework.jdbc.core.ConnectionCallback;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.beans.factory.annotation.Autowired;

import javax.sql.DataSource;
import java.sql.*;
import java.util.*;

/**
 *
 */
public class RowMapContainer
implements
        ConnectionCallback<Object>,
        Container, Container.Ordered,
        Container.Indexed,
        Container.Sortable,
        Container.ItemSetChangeNotifier
{

    @Autowired
    private DataSource dataSource;
    private SqlBuilder sqlBuilder = new PreparedSqlBuilder();
    private ArrayList<String> propertyIds = new ArrayList<String>();
    private LinkedHashMap<Object,RowMapItem> itemMap = new LinkedHashMap<Object,RowMapItem>();

    private LinkedHashMap<String,Class> propertyTypes = new LinkedHashMap<String,Class>();
    private ArrayList<Object> sortableContainerPropertyIds = new ArrayList<Object>();
    private Integer size = -1;
    private Integer index = 0;
    private Integer pageSize = 500;


    public RowMapContainer(  ) {
    }

    public RowMapContainer(DataSource dataSource, String sql) {
        this(dataSource,new PreparedSqlBuilder(sql));
    }

    public RowMapContainer(DataSource dataSource, SqlBuilder sqlBuilder) {
        this.dataSource = dataSource;
        this.sqlBuilder = sqlBuilder;
    }

    public void clear(){
        ArrayList<String> props = new ArrayList<String>(getPropertyIds());
        for(String p:props){
            removeContainerProperty(p);
        }
        removeAllItems();
        sortableContainerPropertyIds.clear();
        notifyItemSetChange();
    }


    public void load() {
        new JdbcTemplate(dataSource).execute(this);
    }

    @Override
    public Object doInConnection(Connection c) throws SQLException, DataAccessException {
        PreparedStatement stmt = sqlBuilder.buildPreparedStatement(c);
        try {
            stmt.setFetchSize(pageSize);
            boolean go=true;
            ResultSet rs = stmt.executeQuery();
            updateMetaProperties(rs);
            int sr = startRow();
            int er = sr+pageSize;
            log.info(String.format("Loading item: fetch=%d,start=%d,end=%d%n%s",pageSize,sr,er,sqlBuilder));
            int row = sr;
            if(sr>0) go = rs.absolute(sr);
            while(go && rs.next() && row < er) {
                RowMapItem rmi = itemRowMapper.mapRow(rs,row);
                itemMap.put(row,rmi);
                row++;
            }
        } finally {
            stmt.close();
        }
        notifyItemSetChange();
        return null;
    }




    private void updateMetaProperties(ResultSet rs) {
        propertyIds.clear();
        propertyTypes.clear();
        sortableContainerPropertyIds.clear();
        try {
            ResultSetMetaData rsmd = rs.getMetaData();
            for(int c=0;c<rsmd.getColumnCount();c++){
                String columnName = rsmd.getColumnName(c+1);
                propertyIds.add(columnName);
                final Property p = getContainerProperty(new Integer(1), columnName);
                propertyTypes.put(columnName, p == null ? Object.class : p.getType());
                sortableContainerPropertyIds.add(columnName);
            }
        } catch (SQLException e) {
            throw new UncategorizedSQLException("Building property ID List",null,e);
        }
        if(log.isDebugEnabled()){
            log.debug("MetaProperties updated: "+propertyIds);
        }
    }


    @Override
    public Item getItem(Object itemId) {
        return itemMap.get(itemId);
    }

    public RowMapItem getRowMapItem(Object itemId) {
        return itemMap.get(itemId);
    }

    @Override
    public Collection<?> getContainerPropertyIds() {
        return propertyIds;
    }

    @Override
    public Collection<?> getItemIds() {
        return Collections.unmodifiableCollection(itemMap.keySet());
    }

    @Override
    public Property getContainerProperty(Object itemId, Object propertyId) {
        if (!(itemId instanceof Integer && propertyId instanceof String)) {
            return null;
        }
        try {
            RowMapItem row = itemMap.get(itemId);
            return row.getItemProperty(propertyId);
        } catch (final Exception e) {
            return null;
        }
    }

    @Override
    public Class<?> getType(Object propertyId) {
        //noinspection SuspiciousMethodCalls
        Class<?> type = propertyTypes.get(propertyId);
        return type==null? Object.class : type;
    }

    @Override
    public int size() {
        return itemMap.size();
    }

    @Override
    public boolean containsId(Object itemId) {
        return itemMap.containsKey(itemId);
    }

    @Override
    public Item addItem(Object itemId) throws UnsupportedOperationException {
        RowMapItem rmi = creatRowMapItem(itemId);
        itemMap.put(itemId,rmi);
        return rmi;
    }

    private RowMapItem creatRowMapItem(Object itemId) {
        RowMapItem rmi = new RowMapItem(itemId,new LinkedHashMap<String,Object>());
        return rmi;
    }

    @Override
    public Object addItem() throws UnsupportedOperationException {
        return addItem(size());
    }

    @Override
    public boolean removeItem(Object itemId) throws UnsupportedOperationException {
        return itemMap.remove(itemId)!=null;
    }

    @Override
    public boolean addContainerProperty(Object propertyId, Class<?> type, Object defaultValue) throws UnsupportedOperationException {
        if(propertyId instanceof String) {
            propertyIds.add((String) propertyId);
            propertyTypes.put((String)propertyId,type);
        } else throw new UnsupportedOperationException();
        return true;
    }

    @SuppressWarnings({"SuspiciousMethodCalls"})
    @Override
    public boolean removeContainerProperty(Object propertyId) throws UnsupportedOperationException {
        propertyIds.remove(propertyId);
        propertyTypes.remove(propertyId);
        return true;
    }

    @Override
    public boolean removeAllItems() throws UnsupportedOperationException {
        itemMap.clear();
        return true;
    }

    @Override
    public int indexOfId(Object itemId) {
        return new ArrayList<Object>(itemMap.keySet()).indexOf(itemId);
    }

    @Override
    public Object getIdByIndex(int index) {
        return new ArrayList<Object>(itemMap.keySet()).get(index);
    }

    @Override
    public Object addItemAt(int index) throws UnsupportedOperationException {
        RowMapItem rmi = (RowMapItem) addItemAt(index,index);
        return rmi.getId();
    }

    public void addItemsAt(int index, Collection<?> objectIds){
        ArrayList<Object> keys = new ArrayList<Object>(itemMap.keySet());
        keys.addAll(index,objectIds);
        LinkedHashMap<Object,RowMapItem> n = new LinkedHashMap<Object,RowMapItem>();
        for(Object o:keys) {
            RowMapItem rmi = itemMap.get(o);
            if(rmi==null) rmi = creatRowMapItem(o);
            n.put( o, rmi);
        }
        itemMap = n;
        notifyItemSetChange();
    }

    @Override
    public Item addItemAt(int index, Object newItemId) throws UnsupportedOperationException {
        addItemsAt(index,Arrays.asList(newItemId));

        return itemMap.get(newItemId);
    }

    @Override
    public Object nextItemId(Object itemId) {
        return itemMap.size();
    }

    @Override
    public Object prevItemId(Object itemId) {
        throw new NotImplementedException();
    }

    @Override
    public Object firstItemId() {
        throw new NotImplementedException();
    }

    @Override
    public Object lastItemId() {
        throw new NotImplementedException();
    }

    @Override
    public boolean isFirstId(Object itemId) {
        throw new NotImplementedException();
    }

    @Override
    public boolean isLastId(Object itemId) {
        throw new NotImplementedException();
    }

    @Override
    public Object addItemAfter(Object previousItemId) throws UnsupportedOperationException {
        throw new NotImplementedException();
    }

    @Override
    public Item addItemAfter(Object previousItemId, Object newItemId) throws UnsupportedOperationException {
        throw new NotImplementedException();
    }


    public void setSql(String sql) throws UncategorizedSQLException {
        setSqlBuilder(new PreparedSqlBuilder(sql));
    }

    @Override
    public void sort(Object[] propertyId, boolean[]
 ascending) {
        if(log.isInfoEnabled()) {
            //noinspection PrimitiveArrayArgumentToVariableArgMethod
            log.info("Sorting container "+Arrays.asList(propertyId)
                    +" - "+Arrays.asList(ascending));
        }
        clear();
        getSqlBuilder().applySortInfo(propertyId,ascending);
        load();
    }


    @Override
    public Collection<Object> getSortableContainerPropertyIds() {
        return sortableContainerPropertyIds;
    }

    public void setSortableContainerPropertyIds(Collection<Object> ids) {
        Collection<Object> s = this.sortableContainerPropertyIds;
        s.clear();
        for(Object o : ids) {
            s.add(o);
        }
    }

    public ArrayList<String> getPropertyIds() {
        return propertyIds;
    }

    public LinkedHashMap<String, Class> getPropertyTypes() {
        return propertyTypes;
    }

    public int getSize() {
        return size;
    }


    private ArrayList<ItemSetChangeListener> itemSetChangeListeners = new ArrayList<ItemSetChangeListener>();

    @Override
    public void addListener(ItemSetChangeListener listener) {
        itemSetChangeListeners.add(listener);
    }

    @Override
    public void removeListener(ItemSetChangeListener listener) {
        itemSetChangeListeners.remove(listener);
    }

    private void notifyItemSetChange() {
        ItemSetChangeEvent e = new ItemSetChangeEvent(){
            @Override
            public Container getContainer() {
                return RowMapContainer.this;
            }
        };
        for(ItemSetChangeListener l:itemSetChangeListeners){
            l.containerItemSetChange(e);
        }
    }



    private RowMapItemMapper itemRowMapper = new RowMapItemMapper();

    protected int startRow(){
        return index*pageSize;
    }

    public SqlBuilder getSqlBuilder() {
        return sqlBuilder;
    }

    public void setSqlBuilder(SqlBuilder sqlBuilder) {
        this.sqlBuilder = sqlBuilder;
    }

    public Integer getPageSize() {
        return pageSize;
    }

    public void setPageSize(Integer pageSize) {
        this.pageSize = pageSize;
    }

    public LinkedHashMap<Object, RowMapItem> getItemMap() {
        return itemMap;
    }

    public void setItemMap(LinkedHashMap<Object, RowMapItem> itemMap) {
        this.itemMap = itemMap;
        notifyItemSetChange();
    }

    public void setItems(List<Map<String,Object>> list) {
        int r=0;
        LinkedHashMap<Object, RowMapItem> itemMap = new LinkedHashMap<Object,RowMapItem>();
        for(Map<String,Object> m:list) {
            itemMap.put(r,new RowMapItem(r,m));
            r++;
        }
        setItemMap(itemMap);
    }

    public RowMapItemMapper getItemRowMapper() {
        return itemRowMapper;
    }

    public void setItemRowMapper(RowMapItemMapper itemRowMapper) {
        this.itemRowMapper = itemRowMapper;
    }

    public ArrayList<ItemSetChangeListener> getItemSetChangeListeners() {
        return itemSetChangeListeners;
    }

    public void setItemSetChangeListeners(ArrayList<ItemSetChangeListener> itemSetChangeListeners) {
        this.itemSetChangeListeners = itemSetChangeListeners;
    }

    public DataSource getDataSource() {
        return dataSource;
    }

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    protected static Logger log = Logger.getLogger(RowMapContainer.class);
}

ItemRowMapper to use as spring row mapper


public class RowMapItemMapper implements RowMapper<RowMapItem> {
    private ColumnMapRowMapper m = new ColumnMapRowMapper();
    @Override
    public RowMapItem mapRow(ResultSet rs, int rowNum) throws SQLException {
        Map<String,Object> r = m.mapRow(rs,rowNum);
        RowMapItem row = new RowMapItem( rowNum, r );
        return row;
    }
}

And the ContainerItem implementation



import com.vaadin.data.Item;
import com.vaadin.data.Property;

import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 *
 */
public class RowMapItem implements Item {

    private Object id;
    private Map<String,Property> propertyMap = new LinkedHashMap<String,Property>();
    private Map<String,Object> contents;

    public Object getId() {
        return id;
    }

    public void setId(Object id) {
        this.id = id;
    }

    public Map<String, Object> getContents() {
        return contents;
    }

    public void setContents(Map<String, Object> contents) {
        this.contents = contents;
    }

    public RowMapItem(Object rowId, Map<String,Object> row) {
        id = rowId;
        contents = row;
        for(String k:row.keySet()){
            Object o = row.get(k);
            propertyMap.put(k,new RowMapProperty( o==null?"":o ));
        }
    }

    /**
     * Adds the item property.
     *
     * @param id
     *            ID of the new Property.
     * @param property
     *            Property to be added and associated with ID.
     * @return <code>true</code> if the operation succeeded;
     *         <code>false</code> otherwise.
     * @throws UnsupportedOperationException
     *             if the addItemProperty method is not supported.
     */
    public boolean addItemProperty(Object id, Property property)
            throws UnsupportedOperationException {
        propertyMap.put( id.toString(), property );
        return true;
    }

    /**
     * Gets the property corresponding to the given property ID stored in
     * the Item.
     *
     * @param propertyId
     *            identifier of the Property to get
     * @return the Property with the given ID or <code>null</code>
     */
    public Property getItemProperty(Object propertyId) {
        if(!(propertyId instanceof String)) return null;
        //noinspection SuspiciousMethodCalls
        return propertyMap.get(propertyId.toString());
    }

    /**
     * Gets the collection of property IDs stored in the Item.
     *
     * @return unmodifiable collection containing IDs of the Properties
     *         stored the Item.
     */
    public Collection getItemPropertyIds() {
        return Collections.unmodifiableCollection(propertyMap.keySet());
    }

    /**
     * Removes given item property.
     *
     * @param id
     *            ID of the Property to be removed.
     * @return <code>true</code> if the item property is removed;
     *         <code>false</code> otherwise.
     * @throws UnsupportedOperationException
     *             if the removeItemProperty is not supported.
     */
    public boolean removeItemProperty(Object id)
            throws UnsupportedOperationException {
        return propertyMap.remove(id.toString())!=null;
    }

    public class RowMapProperty implements Property {

        private boolean readOnly = true;
        private Object value;

        public RowMapProperty(Object value) {
            this(value,true);
        }

        private RowMapProperty(Object value,boolean readOnly) {
            this.value = value;
            this.readOnly = readOnly;
        }

        @Override
        public Object getValue() {
            return value;
        }

        @Override
        public void setValue(Object newValue) throws ReadOnlyException, ConversionException {
            this.value = newValue;
        }

        @Override
        public Class<?> getType() {
            return value!=null? value.getClass() : null;
        }

        @Override
        public boolean isReadOnly() {
            return readOnly;
        }

        @Override
        public void setReadOnly(boolean newStatus) {
            this.readOnly = newStatus;
        }

        public String toString() {
            return value==null ? "" : String.valueOf(value);
        }

        public Item getItem(){
            return RowMapItem.this;
        }
    }
}