JSF in SF

Wednesday, March 22, 2006

Usings Sets with UIData

One of the more personally surprising usability complaints I've heard was that the JSF UIData component does not support Sets. I hadn't anticipated that one. The underlying reason is that UIData is built around indexed data. For example, you can ask it to show rows 10,000 through 10,099. Such an operation would be nightmarishly expensive in a Set:



Iterator iter = collection.iterator();
for (int i = 0; i < 10000; i++) { iter.next(); }
// Hey, now we can start reading our data!



But, hey, we still got it wrong: we should have supported it. Yeah, it'd be slow - O(N) where N is the size of the set, not the amount of data actually displayed at one time - but the ease of use argument is compelling.

That said, there's nothing stopping you from using Sets with UIData right now... if you use the following class:


import java.util.AbstractMap;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.RandomAccess;
import java.util.Set;

public class ListFromCollection
{
public ListFromCollection()
{
_map = new MakeList();
_size = _DEFAULT_SIZE;
}

public Map<Collection, List> getList()
{
return _map;
}

public int getSize()
{
return _size;
}

public void setSize(int size)
{
_size = size;
}

private class MakeList extends AbstractMap<Collection, List>
{
public List get(Object o)
{
if (!(o instanceof Collection))
return null;

// Just send RandomAccess lists out; wrap any other Collection
// into a List
if ((o instanceof List) &&
(o instanceof RandomAccess))
return (List) o;

Collection c = (Collection) o;
if (c.isEmpty())
return Collections.EMPTY_LIST;

return new ListImpl(c, getSize());
}

public Set<Map.Entry<Collection, List>> entrySet()
{
// Not worth implementing at the moment; this Map is only
// accessed from
return Collections.emptySet();
}
}

static private class ListImpl extends AbstractList
{
public ListImpl(Collection c, int size)
{
_c = c;
_cSize = c.size();
if (size == 0)
_bufferSize = _cSize;
else
_bufferSize = Math.min(size, _cSize);

_buffer = new ArrayList(_bufferSize);
_offset = -1;
}

public int size()
{
return _cSize;
}

public Object get(int index)
{
if ((index < 0) || (index >= _cSize))
throw new IndexOutOfBoundsException();

int offset = (index / _bufferSize) * _bufferSize;
if (offset != _offset)
{
_loadBuffer(offset);
_offset = offset;
}

return _buffer.get(index - _offset);
}

private void _loadBuffer(int offset)
{
Iterator iter = _c.iterator();
int i = 0;

while (i < offset)
{
assert iter.hasNext();
iter.next();
i++;
}

_buffer.clear();

int count = 0;
while ((count < _bufferSize) && (i < _cSize))
{
assert iter.hasNext();
_buffer.add(iter.next());
i++;
count++;
}
}

private final Collection _c;
private final int _bufferSize;
private final int _cSize;
private int _offset;
private ArrayList _buffer;
}

private Map<Collection, List> _map;
private int _size;

static private int _DEFAULT_SIZE = 50;
}


So, what's this all mean? Well, say you want to write:


<h:dataTable value="#{mySet}">


... but that doesn't work. Just add one managed-bean entry to your faces-config.xml:


<managed-bean>
<managed-bean-name>makeList</managed-bean-name>
<managed-bean-class>
yourPackageHere.ListFromCollection
</managed-bean-class>
<managed-bean-scope>
request
</managed-bean-scope>
</managed-bean>


... and now, you can use sets on dataTable via:


<h:dataTable value="#{makeList.list[mySet]}">

201 Comments:

Post a Comment

<< Home