XPages MVC: Experiment II, Part 3
May 28, 2012, 8:00 PM
- XPages MVC: Experiment I
- XPages MVC: Experiment II, Part 1
- XPages MVC: Experiment II, Part 2
- XPages MVC: Experiment II, Part 3
- XPages MVC: Experiment II, Part 4
Continuing on from my last post, I'd like to go over a couple specifics about how I handle fetching appropriate collections of objects from a view and a couple areas where I saved myself some programming hassle.
As I mentioned before, both the "manager" and "collection" classes inherit from abstract classes that handle a lot of the dirty work. The AbstractCollectionManager
class is by far the smaller of the two, containing mostly convenience methods and a couple overloaded methods for generating the collection objects. The central one is pretty straightforward:
protected AbstractDominoList<E> createCollection(
String sortBy, Object restrictTo, String searchQuery, int preCache, boolean singleEntry) {
AbstractDominoList<E> collection = this.createCollection();
collection.setSortBy(sortBy);
collection.setRestrictTo(restrictTo);
collection.setSearchQuery(searchQuery);
collection.setSingleEntry(singleEntry);
collection.preCache(preCache);
return collection;
}
The first three properties actually determine the nature of the collection, while the last two set some optimization bits when the code knows ahead of time about how many elements it expects (such as doing a by-id lookup or a "recent news" list that will only ever display 5 entries). The createCollection
call at the start is overridden method in each class that returns a basic collection object to avoid Java visibility issues.
The AbstractDominoList
class is much longer; I won't go into too much detail, but a couple parts are pertinent. As I've mentioned, it implements the List
interface (via extending AbstractList
), which means it just needs to provide get
and size
methods. The way these work is by checking to see if it houses a valid cached ViewEntryCollection
and, if it does, translating the method call to get the indexed entry as an object or the size. If the collection hasn't been fetched yet or if it's no longer valid (say, if it was recycled), it uses the view-location information from the implementing class and any parameters on the object to construct another one.
First, it fetches and configures the view properly:
Database database = this.getDatabase();
View view = database.getView(this.getViewName());
view.refresh();
String sortColumn = this.sortBy == null ? null :
this.sortBy.toLowerCase().endsWith("-desc") ? this.sortBy.substring(0, this.sortBy.length()-5) :
this.sortBy;
boolean sortAscending = this.sortBy == null || !this.sortBy.toLowerCase().endsWith("-desc");
if(this.searchQuery != null) {
view.FTSearchSorted(this.searchQuery, 0, sortColumn, sortAscending, false, false, false);
} else if(this.sortBy != null && !this.sortBy.equals(this.getDefaultSort())) {
view.resortView(sortColumn, sortAscending);
} else {
view.resortView();
}
Then, it uses code similar to this to store the collection in its cache (I say "similar to" because there are additional branches and methods, but this is the idea):
if(this.restrictTo == null) {
ViewEntryCollection collection = view.getAllEntries();
this.cachedEntryCollection = new DominoEntryCollectionWrapper(collection, view);
} else {
ViewEntryCollection collection = view.getAllEntriesByKey(this.restrictTo, true);
this.cachedEntryCollection = new DominoEntryCollectionWrapper(collection, view);
}
The other part of the get
method is translating the ViewEntry
into an object, which is done via a method similar to this monster:
protected E createObjectFromViewEntry(ViewEntry entry) throws Exception {
Class currentClass = this.getClass();
Class modelClass = Class.forName(JSFUtil.strLeftBack(currentClass.getName(), ".") + "." + JSFUtil.strLeftBack(currentClass.getSimpleName(), "List"));
// Create an instance of our model object
E object = (E)modelClass.newInstance();
object.setUniversalId(entry.getUniversalID());
object.setDocExists(true);
// Loop through the declared columns and extract the values from the Entry
Method[] methods = modelClass.getMethods();
String[] columns = this.getColumnFields();
Vector values = entry.getColumnValues();
for(Method method : methods) {
for(int i = 0; i < columns.length; i++) {
if(values != null && i < values.size() && columns[i].length() > 0 && method.getName().equals("set" + columns[i].substring(0, 1).toUpperCase() + columns[i].substring(1))) {
String fieldType = method.getGenericParameterTypes()[0].toString();
if(fieldType.equals("int")) {
if(values.get(i) instanceof Double) {
method.invoke(object, ((Double)values.get(i)).intValue());
}
} else if(fieldType.equals("class java.lang.String")) {
method.invoke(object, values.get(i).toString());
} else if(fieldType.equals("java.util.List<java.lang.String>")) {
method.invoke(object, JSFUtil.toStringList(values.get(i)));
} else if(fieldType.equals("class java.util.Date")) {
if(values.get(i) instanceof DateTime) {
method.invoke(object, ((DateTime)values.get(i)).toJavaDate());
}
} else if(fieldType.equals("boolean")) {
if(values.get(i) instanceof Double) {
method.invoke(object, ((Double)values.get(i)).intValue() == 1);
} else {
method.invoke(object, values.get(i).toString().equals("Yes"));
}
} else if(fieldType.equals("double")) {
if(values.get(i) instanceof Double) { method.invoke(object, (Double)values.get(i)); }
} else if(fieldType.equals("java.util.List<java.lang.Integer>")) {
method.invoke(object, JSFUtil.toIntegerList(values.get(i)));
} else {
System.out.println("!! unknown type " + fieldType);
}
}
}
}
return object;
}
Yes, I am aware that it's O(m*n). Yes, I am aware I could move values != null
outside the loop. Yes, I am aware I do the same string manipulation on each column name many times. Shut up.
Anyway, that code finds the model class and loops through its methods, looking for set*
methods for each column (the column list being specified in the implementation class). When it finds one that matches, it checks the data type of the property, and invokes the method with an appropriately-cast or -converted column value. The result is that the implementation class only needs to provide the code needed for fetching any data that isn't in the view, such as rich text. At one point, I think I had it so that it read the view column titles and used those as field names, but I stopped doing that for some reason... maybe it was too slow, even for this method.
In any event, next time, I'll go over some of the disadvantages that I've found with this method that don't involve big-O notation.