/** * Copyright (c) 2011, Thilo Planz. All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package v7cr.vaadin; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.bson.BSONObject; import v7cr.v7db.SchemaDefinition; import com.mongodb.BasicDBObject; import com.mongodb.DBCollection; import com.mongodb.DBCursor; import com.mongodb.DBObject; import com.vaadin.data.Item; import com.vaadin.data.Property; import com.vaadin.data.Container.Indexed; import com.vaadin.data.Container.Ordered; import com.vaadin.data.util.AbstractContainer; import com.vaadin.data.util.BeanContainer; /** * A Vaadin Container backed by a MongoDB collection. * * <p> * It is "partially lazy-loading": Upon construction, it loads all documents' * _id fields (so it probably does not work with capped collections) into an * ordered in-memory collection. The rest of the document gets loaded on demand * and in pages. * * <p> * Every document becomes a Vaadin Item. Since MongoDB is schema-free, there is * no database metadata to get the propertyIds for these items. If you do not * specify any, the first page of documents is loaded and all top-level * non-object field names found there are added as propertyIds. The types of * these properties are set to the first value found. * * <p> * You can add nested properties ("a.x") manually, just like with the * {@link BeanContainer}. * * * @author Thilo Planz, based on the Vaadin SQLContainer addon * */ public class DBCollectionContainer extends AbstractContainer implements Ordered, Indexed { private final List<Object> _ids; private final DBCollection collection; /** Container properties = column names, data types and statuses */ private final List<String> propertyIds = new ArrayList<String>(); private final Map<String, Class<?>> propertyTypes = new HashMap<String, Class<?>>(); private final Map<String, Boolean> propertyReadOnly = new HashMap<String, Boolean>(); private final Map<String, Boolean> propertyNullable = new HashMap<String, Boolean>(); /** Page length = number of items contained in one page */ private final int pageLength = DEFAULT_PAGE_LENGTH; public static final int DEFAULT_PAGE_LENGTH = 100; /** Starting row number of the currently fetched page */ private int currentOffset = -1; /** Item and index caches */ private final Map<Integer, Object> itemIndexes = new HashMap<Integer, Object>(); private final Map<Object, BSONItem> cachedItems = new HashMap<Object, BSONItem>(); public DBCollectionContainer(DBCollection collection, DBCursor cursor) { this.collection = collection; _ids = initIds(cursor); initPropertyIds(); } private List<Object> initIds(DBCursor cursor) { List<DBObject> x = cursor.toArray(); List<Object> _ids = new ArrayList<Object>(x.size()); for (DBObject o : x) { _ids.add(o.get("_id")); } return _ids; } private void initPropertyIds() { if (propertyIds.isEmpty()) { getPage(); for (BSONItem b : cachedItems.values()) for (String s : b.getBSONObject().keySet()) if (!propertyIds.contains(s)) if (!(b.getBSONObject().get(s) instanceof BSONObject)) { propertyIds.add(s); propertyTypes.put(s, b.getBSONObject().get(s) .getClass()); } } } private void initPropertyIds(SchemaDefinition schema) { if (schema != null) for (String s : schema.getFieldNames()) if (!propertyIds.contains(s)) { propertyIds.add(s); propertyTypes.put(s, String.class); } initPropertyIds(); } public DBCollectionContainer(DBCollection collection, String sortBy, boolean ascending) { this(null, collection, null, sortBy, ascending); } public DBCollectionContainer(SchemaDefinition schema, DBCollection collection, DBObject filter, String sortBy, boolean ascending) { this.collection = collection; DBObject _null = new BasicDBObject(); if (filter == null) filter = _null; DBCursor cursor = collection.find(filter, _null); if (sortBy != null) { cursor = cursor.sort(new BasicDBObject(sortBy, ascending ? 1 : -1)); } _ids = initIds(cursor); initPropertyIds(schema); } private void getPage() { if (_ids.isEmpty()) return; int fromIndex; if (currentOffset < 0) { fromIndex = 0; } else { cachedItems.clear(); itemIndexes.clear(); fromIndex = currentOffset + pageLength; } int toIndex = fromIndex + pageLength; if (toIndex >= _ids.size()) { toIndex = _ids.size() - 1; } // TODO: is there a way to do bulk loading? int idx = fromIndex; for (Object id : _ids.subList(fromIndex, toIndex)) { // TODO: only load the fields we need cachedItems.put(id, new BSONItem(collection.findOne(id))); itemIndexes.put(idx++, id); } currentOffset = fromIndex; } public boolean addContainerProperty(Object propertyId, Class<?> type, Object defaultValue) throws UnsupportedOperationException { if (propertyIds.contains(propertyId)) return false; if (!(propertyId instanceof String)) return false; String pid = (String) propertyId; propertyIds.add(pid); propertyTypes.put(pid, type); return true; } public Object addItem() throws UnsupportedOperationException { // TODO Auto-generated method stub return null; } public Item addItem(Object itemId) throws UnsupportedOperationException { // TODO Auto-generated method stub return null; } public boolean containsId(Object itemId) { return _ids.contains(itemId); } public Property getContainerProperty(Object itemId, Object propertyId) { Item item = getItem(itemId); if (item == null) { return null; } return item.getItemProperty(propertyId); } public Collection<?> getContainerPropertyIds() { return Collections.unmodifiableCollection(propertyIds); } public Item getItem(Object itemId) { BSONItem cached = cachedItems.get(itemId); if (cached != null) return cached; BSONObject b = collection.findOne(itemId); if (b == null) return null; BSONItem i = new BSONItem(b); cachedItems.put(itemId, i); return i; } public Collection<?> getItemIds() { return Collections.unmodifiableCollection(_ids); } public Class<?> getType(Object propertyId) { return propertyTypes.get(propertyId); } public boolean removeAllItems() throws UnsupportedOperationException { // TODO Auto-generated method stub return false; } public boolean removeContainerProperty(Object propertyId) throws UnsupportedOperationException { // TODO Auto-generated method stub return false; } public boolean removeItem(Object itemId) throws UnsupportedOperationException { // TODO Auto-generated method stub return false; } public int size() { return _ids.size(); } public Object addItemAfter(Object previousItemId) throws UnsupportedOperationException { // TODO Auto-generated method stub return null; } public Item addItemAfter(Object previousItemId, Object newItemId) throws UnsupportedOperationException { // TODO Auto-generated method stub return null; } public Object firstItemId() { return _ids.get(0); } public boolean isFirstId(Object itemId) { if (!_ids.get(0).equals(itemId)) return false; return true; } public boolean isLastId(Object itemId) { int idx = _ids.size() - 1; if (!_ids.get(idx).equals(itemId)) return false; return true; } public Object lastItemId() { int idx = _ids.size() - 1; return _ids.get(idx); } public Object nextItemId(Object itemId) { // TODO: linear search, not so nice boolean theNext = false; for (Object id : _ids) { if (theNext) return id; if (itemId.equals(id)) theNext = true; } return null; } public Object prevItemId(Object itemId) { // TODO: linear search, not so nice Object prev = null; for (Object id : _ids) { if (itemId.equals(id)) return prev; prev = id; } return null; } public Object addItemAt(int index) throws UnsupportedOperationException { // TODO Auto-generated method stub return null; } public Item addItemAt(int index, Object newItemId) throws UnsupportedOperationException { // TODO Auto-generated method stub return null; } public Object getIdByIndex(int index) { return _ids.get(index); } public int indexOfId(Object itemId) { return _ids.indexOf(itemId); } }