Java tutorial
/** * Copyright (c) 2014 - Tyl Consulting s.a.s. * * Authors: Edoardo Vacchi * Contributors: Marco Pancotti, Daniele Zonca * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.tylproject.vaadin.addon; import com.mongodb.BasicDBObject; import com.mongodb.DBCollection; import com.mongodb.DBCursor; import com.mongodb.DBObject; import com.vaadin.data.Container; import com.vaadin.data.Item; import com.vaadin.data.Property; import com.vaadin.data.util.AbstractContainer; import com.vaadin.data.util.BeanItem; import com.vaadin.data.util.filter.UnsupportedFilterException; import org.springframework.beans.BeanUtils; import org.springframework.data.domain.Sort; import org.tylproject.vaadin.addon.beanfactory.BeanFactory; import org.tylproject.vaadin.addon.beanfactory.DefaultBeanFactory; import org.tylproject.vaadin.addon.utils.DefaultFilterConverter; import org.tylproject.vaadin.addon.utils.FilterConverter; import org.tylproject.vaadin.addon.utils.Page; import org.bson.types.ObjectId; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.beans.*; import java.util.*; import java.util.logging.Logger; import static org.springframework.data.mongodb.core.query.Criteria.where; /** * Simple (non-buffered) Mongo Container * * Every change to this container will be immediately reflected in the DB. * An instance of this container can be obtained through the fluent * {@link org.tylproject.vaadin.addon.MongoContainer.Builder} * * This container is Ordered but not {@link com.vaadin.data.Container.Sortable}, * because it is not possible to sort it on the fly. You can always * build a container for a sorted query using the Builder method * {@link org.tylproject.vaadin.addon.MongoContainer.Builder#sortedBy(org.springframework.data.domain.Sort)}. * * Please notice that it is not possible to {@link #addItem()} * or {@link #addItem(Object)}, because it would not be possible to satisfy * the contract of the method. The itemId should be a temporary handle, * but this container immediately reflects changes onto the DB, * so this would result in an empty entity on the DB. * * */ public class MongoContainer<Bean> extends AbstractContainer implements Container, Container.Ordered, Container.Indexed, Container.Filterable, Container.Sortable, Container.ItemSetChangeNotifier { /** * Fluent Builder for a (Buffered)MongoContainer instance. * * Every Container includes all the properties of the given bean as a default. * You can use the methods {@link org.tylproject.vaadin.addon.MongoContainer.Builder#withProperty(java.lang.String, Class)} * and {@link org.tylproject.vaadin.addon.MongoContainer.Builder#withNestedProperty(java.lang.String, Class)} * to define a custom list of properties. * * @param <BT> Type of the entity */ public static class Builder<BT> { private final static int DEFAULT_PAGE_SIZE = 100; private final MongoOperations mongoOps; private Criteria mongoCriteria = new Criteria(); private final Class<BT> beanClass; private Sort sort; private int pageSize = DEFAULT_PAGE_SIZE; private Map<String, Class<?>> simpleProperties = new LinkedHashMap<String, Class<?>>(); private Map<String, Class<?>> nestedProperties = new LinkedHashMap<String, Class<?>>(); private boolean hasCustomPropertyList = false; private boolean hasNestedPropertyList = false; private BeanFactory<BT> beanFactory; private FilterConverter filterConverter = new DefaultFilterConverter(); public String parentProperty; /** * Initializes and return a builder for a MongoContainer * * @param beanClass class of the entity * @param mongoOps mongoOperation instance * @param <T> type of the entity * @return the builder instance for the given entity, * using the given MongoOperations instance */ public static <T> MongoContainer.Builder<T> forEntity(final Class<T> beanClass, final MongoOperations mongoOps) { return new MongoContainer.Builder<T>(beanClass, mongoOps); } private Builder(final Class<BT> beanClass, final MongoOperations mongoOps) { this.mongoOps = mongoOps; this.beanClass = beanClass; this.beanFactory = new DefaultBeanFactory<BT>(beanClass); } public Builder<BT> withBeanFactory(BeanFactory<BT> beanFactory) { this.beanFactory = beanFactory; return this; } /** * @param mongoCriteria A {@link org.springframework.data.mongodb.core.query.Criteria} * object created through Spring's * fluent interface */ public Builder<BT> forCriteria(final Criteria mongoCriteria) { this.mongoCriteria = mongoCriteria; return this; } /** * @param sort A Spring {@link org.springframework.data.domain.Sort} object */ public Builder<BT> sortedBy(final Sort sort) { this.sort = sort; return this; } /** * specify the (internal) page size of the lazy container */ public Builder<BT> withPageSize(final int pageSize) { this.pageSize = pageSize; return this; } /** * adds a property with the given property id and of the given type */ public Builder<BT> withProperty(String id, Class<?> type) { hasCustomPropertyList = true; simpleProperties.put(id, type); return this; } /** * adds a property with the given property id and of the given type */ public Builder<BT> withProperty(String id) { return withProperty(id, BeanUtils.findPropertyType(id, beanClass)); } /** * adds a nested property of the given type. * * A <em>nested</em> property for a bean is a property that can be reached * through a <i>path</i> like <code>path.to.property</code>. * * e.g., suppose you have a bean Person, with a property "address" * of type Address; suppose that Address has a property "street". * In code you can write <code>myPerson.getAddress().getStreet()</code>. * * You can access such nested properties in the container using the syntax: * <pre> * builder.withNestedProperty("address.street"); * </pre> * */ public Builder<BT> withNestedProperty(String id, Class<?> type) { hasNestedPropertyList = true; nestedProperties.put(id, type); return this; } public Builder<BT> withFilterConverter(FilterConverter customFilterConverter) { this.filterConverter = customFilterConverter; return this; } /** * @return a simple MongoContainer instance */ public MongoContainer<BT> build() { final MongoContainer<BT> mc = new MongoContainer<BT>(this); mc.fetchPage(0, pageSize); return mc; } /** * @return a BufferedMongoContainer instance */ public BufferedMongoContainer<BT> buildBuffered() { final BufferedMongoContainer<BT> mc = new BufferedMongoContainer<BT>(this); mc.fetchPage(0, pageSize); return mc; } public HierarchicalMongoContainer<BT> buildHierarchical(String id) { this.parentProperty = id; final HierarchicalMongoContainer<BT> mc = new HierarchicalMongoContainer<BT>(this); mc.fetchPage(0, pageSize); return mc; } } protected static final String ID = "_id"; protected static final Logger log = Logger.getLogger("MongoContainer"); @Nonnull protected Page<ObjectId> page; protected final int pageSize; protected final Criteria criteria; /** * criteria updated by {@link #addContainerFilter(com.vaadin.data.Container.Filter)} */ protected Query query; protected final Query baseQuery; protected final Sort baseSort; protected Sort sort; protected final FilterConverter filterConverter; protected final List<Filter> appliedFilters = new ArrayList<Filter>(); protected final List<Criteria> appliedCriteria = new ArrayList<Criteria>(); protected final MongoOperations mongoOps; protected final Class<Bean> beanClass; protected final BeanFactory<Bean> beanFactory; protected final Map<String, Class<?>> simpleProperties; protected final Map<String, Class<?>> nestedProperties; protected final List<Object> allProperties; MongoContainer(Builder<Bean> bldr) { this.criteria = bldr.mongoCriteria; this.baseSort = bldr.sort; this.filterConverter = bldr.filterConverter; this.baseQuery = Query.query(criteria).with(baseSort); resetQuery(); this.mongoOps = bldr.mongoOps; this.beanClass = bldr.beanClass; this.beanFactory = bldr.beanFactory; if (bldr.hasCustomPropertyList) { this.simpleProperties = Collections.unmodifiableMap(bldr.simpleProperties); } else { // otherwise, get them via reflection this.simpleProperties = new LinkedHashMap<String, Class<?>>(); PropertyDescriptor[] descriptors = BeanUtils.getPropertyDescriptors(beanClass); for (PropertyDescriptor d : descriptors) { this.simpleProperties.put(d.getName(), d.getPropertyType()); } } if (bldr.hasNestedPropertyList) { this.nestedProperties = Collections.unmodifiableMap(bldr.nestedProperties); } else { nestedProperties = Collections.emptyMap(); } List<Object> allProps = new ArrayList<Object>(simpleProperties.keySet()); // remove "class" pseudo-property for compliance with BeanItem allProps.remove("class"); this.allProperties = Collections.unmodifiableList(allProps); allProps.addAll(nestedProperties.keySet()); this.pageSize = bldr.pageSize; } /** * @return a cursor for the query object of this Container instance */ protected DBCursor cursor() { return cursor(null); } /** * @return a cursor with the given optional params */ protected DBCursor cursor(DBObject additionalCriteria) { final Query q = this.query; DBObject criteriaObject = q.getQueryObject(); if (additionalCriteria != null) { criteriaObject.putAll(additionalCriteria); } DBObject projectionObject = new BasicDBObject(ID, true); String collectionName = mongoOps.getCollectionName(beanClass); DBCollection dbCollection = mongoOps.getCollection(collectionName); // TODO: keep cursor around to possibly reuse DBCursor cursor = dbCollection.find(criteriaObject, projectionObject); if (this.baseSort != null || this.sort != null) { DBObject sortObject = q.getSortObject(); cursor.sort(sortObject); } return cursor; } /** * returns a cursor in the given range. * * shorthand for <pre> * cursor().skip(skip).limit(limit); * </pre> */ protected DBCursor cursorInRange(int skip, int limit) { return cursor().skip(skip).limit(limit); } /** * fetches a {@link org.tylproject.vaadin.addon.utils.Page} * within the given range * */ protected void fetchPage(int offset, int pageSize) { // TODO: keep cursor around to possibly reuse DBCursor cursor = cursorInRange(offset, pageSize); Page<ObjectId> newPage = new Page<ObjectId>(pageSize, offset, this.size()); for (int i = offset; cursor.hasNext(); i++) newPage.set(i, (ObjectId) cursor.next().get(ID)); this.page = newPage; } /** * returns the current page and refreshes it when invalid */ protected Page<ObjectId> page() { if (!page.isValid()) fetchPage(page.offset, page.pageSize); return page; } public BeanItem<Bean> getItem(Object o) { if (o == null) return null; assertIdValid(o); final Bean document = mongoOps.findById(o, beanClass); // document was not found in the actual DB // but it was in the ID cache // then the cache is invalid if (document == null && page.contains(o)) { refresh(); } return makeBeanItem(document); } protected BeanItem<Bean> makeBeanItem(Bean document) { if (document == null) return null; final BeanItem<Bean> beanItem = new BeanItem<Bean>(document, this.simpleProperties.keySet()); for (String nestedPropId : nestedProperties.keySet()) { beanItem.addNestedProperty(nestedPropId); } return beanItem; } @Override public Collection<?> getContainerPropertyIds() { return this.allProperties; } // method is basically deprecated @Override @Deprecated public List<ObjectId> getItemIds() { log.info("this expensive operation should be avoided"); return getItemIds(0, this.size()); } @Override public Property<?> getContainerProperty(Object itemId, Object propertyId) { BeanItem<Bean> item = getItem(itemId); if (item == null) return null; return item.getItemProperty(propertyId); } // return the data type of the given property id @Override public Class<?> getType(Object propertyId) { if (simpleProperties.containsKey(propertyId)) return simpleProperties.get(propertyId); else if (nestedProperties.containsKey(propertyId)) return nestedProperties.get(propertyId); throw new IllegalArgumentException("Cannot find the given propertyId: " + propertyId); } @Override public int size() { return (int) mongoOps.count(query, beanClass); } @Override public boolean containsId(Object itemId) { if (itemId == null) return false; assertIdValid(itemId); Query q = makeBaseQuery().addCriteria(where(ID).is(itemId)); return mongoOps.exists(q, beanClass); } /** * @throws UnsupportedOperationException */ @Override public BeanItem<Bean> addItem(Object itemId) throws UnsupportedOperationException { throw new UnsupportedOperationException( "cannot addItem(); insert() into mongo or build a buffered container"); } /** * @throws UnsupportedOperationException */ @Override public ObjectId addItem() throws UnsupportedOperationException { throw new UnsupportedOperationException( "cannot addItem(); insert() into mongo or build a buffered container"); } /** * performs an upsert of the given target bean */ public ObjectId addEntity(Bean target) { mongoOps.save(target); refresh(); fireItemSetChange(); return this.beanFactory.getId(target); } @Override public boolean removeItem(Object itemId) throws UnsupportedOperationException { Query q = makeBaseQuery().addCriteria(where(ID).is(itemId)); mongoOps.findAndRemove(q, beanClass); refresh(); fireItemSetChange(); return true; } @Override public boolean addContainerProperty(Object o, Class<?> aClass, Object o2) throws UnsupportedOperationException { throw new UnsupportedOperationException("cannot add container property dynamically; use Builder"); } @Override public boolean removeContainerProperty(Object o) throws UnsupportedOperationException { throw new UnsupportedOperationException(); } @Override public boolean removeAllItems() throws UnsupportedOperationException { mongoOps.remove(this.query, beanClass); refresh(); fireItemSetChange(); return true; } @Override public int indexOfId(Object itemId) { if (itemId == null) return -1; ObjectId oid = assertIdValid(itemId); // for the principle of locality, // let us optimistically first check within the page int index = page().indexOf(oid); if (index > -1) return index; // otherwise, linearly scan the entire collection using a cursor // and only fetch the ids DBCursor cur = cursor(); for (int i = 0; cur.hasNext(); i++) { // skip the check for those already in the page DBObject value = cur.next(); if (i >= page.offset && i < page.maxIndex) { continue; } if (value.get(ID).equals(itemId)) return i; } return -1; } @Override @Nullable public ObjectId getIdByIndex(int index) { if (index < 0 || size() == 0) return null; DBCursor cur = cursorInRange(index, 1); return cur.hasNext() ? (ObjectId) cur.next().get(ID) : null; } @Override public List<ObjectId> getItemIds(int startIndex, int numberOfItems) { //List<BeanId> beans = mongoOps.find(Query.query(criteria).skip(startIndex).limit(numberOfItems), BeanId.class); //List<ObjectId> ids = new PropertyList<ObjectId,BeanId>(beans, beanIdDescriptor, "_id"); log.info(String.format("range: [%d,%d]", startIndex, numberOfItems)); if (page.isValid() && page.isWithinRange(startIndex, numberOfItems)) { return page.subList(startIndex, numberOfItems); // return the requested range } fetchPage(startIndex, numberOfItems); return this.page.toImmutableList(); } @Override public Object addItemAt(int index) throws UnsupportedOperationException { throw new UnsupportedOperationException(); } @Override public Item addItemAt(int index, Object newItemId) throws UnsupportedOperationException { throw new UnsupportedOperationException(); } @Override public ObjectId nextItemId(Object itemId) { int index = indexOfId(itemId); return getIdByIndex(index + 1); } @Override public ObjectId prevItemId(Object itemId) { int index = indexOfId(itemId); return getIdByIndex(index - 1); } @Override public ObjectId firstItemId() { return getIdByIndex(0); } @Override public ObjectId lastItemId() { return size() > 0 ? getIdByIndex(size() - 1) : null; } @Override public boolean isFirstId(Object itemId) { if (itemId == null) return false; assertIdValid(itemId); return itemId.equals(firstItemId()); } @Override public boolean isLastId(Object itemId) { if (itemId == null) return false; assertIdValid(itemId); return itemId.equals(lastItemId()); } @Override public Object addItemAfter(Object previousItemId) throws UnsupportedOperationException { throw new UnsupportedOperationException(); } @Override public Item addItemAfter(Object previousItemId, Object newItemId) throws UnsupportedOperationException { throw new UnsupportedOperationException(); } @Override public void addItemSetChangeListener(ItemSetChangeListener listener) { super.addItemSetChangeListener(listener); } @Override public void addListener(ItemSetChangeListener listener) { super.addListener(listener); } @Override public void removeItemSetChangeListener(ItemSetChangeListener listener) { super.removeItemSetChangeListener(listener); } @Override public void removeListener(ItemSetChangeListener listener) { super.removeListener(listener); } @Override protected void fireItemSetChange() { page.setInvalid(); super.fireItemSetChange(); } /** * invalidate the internal page and reload it */ public void refresh() { page.setInvalid(); page(); } protected Query makeBaseQuery() { return Query.query(criteria).with(baseSort); } /** * Verifies that the given Object instance is a valid itemId * * Note: This should generally be done through type-checking of the parameter * but this is not possible because of Vaadin's interfaces * * @throws java.lang.NullPointerException if the parameter is null * @throws java.lang.IllegalArgumentException if the parameter is not an ObjectId */ protected ObjectId assertIdValid(Object o) { if (o == null) throw new NullPointerException("Id cannot be null"); if (!(o instanceof ObjectId)) throw new IllegalArgumentException("Id is not instance of ObjectId: " + o); return (ObjectId) o; } @Override public void addContainerFilter(Filter filter) throws UnsupportedFilterException { Criteria c = filterConverter.convert(filter); if (this.query.getQueryObject().toMap().isEmpty()) { this.query = new Query(c); } else { this.query.addCriteria(c); } appliedCriteria.add(c); appliedFilters.add(filter); page.setInvalid(); fireItemSetChange(); } public void addAllContainerFilters(Collection<? extends Filter> filters) { for (Filter f : filters) this.addContainerFilter(f); } @Override public void removeContainerFilter(Filter filter) { appliedFilters.remove(filter); List<Filter> backupFilters = new ArrayList<Filter>(appliedFilters); // the only way to re-build the query is clearing it and rebuild it // so, we clear the appliedFilters list doRemoveAllContainerFilters(); // and we add them back this.addAllContainerFilters(backupFilters); fireItemSetChange(); } @Override public void removeAllContainerFilters() { doRemoveAllContainerFilters(); fireItemSetChange(); } protected void doRemoveAllContainerFilters() { resetQuery(); applySort(this.query, this.sort); page.setInvalid(); } protected void resetQuery() { this.query = makeBaseQuery(); this.appliedFilters.clear(); this.appliedCriteria.clear(); this.sort = null; } @Override public Collection<Filter> getContainerFilters() { return Collections.unmodifiableList(new ArrayList<Filter>(appliedFilters)); } @Override public void sort(Object[] propertyId, boolean[] ascending) { if (propertyId.length != ascending.length) throw new IllegalArgumentException( String.format("propertyId array length does not match" + "ascending array length (%d!=%d)", propertyId.length, ascending.length)); Sort result = null; // if the arrays are empty, will just the conditions if (propertyId.length != 0) { result = new Sort(ascending[0] ? Sort.Direction.ASC : Sort.Direction.DESC, propertyId[0].toString()); for (int i = 1; i < propertyId.length; i++) { result = result.and(new Sort(ascending[i] ? Sort.Direction.ASC : Sort.Direction.DESC, propertyId[i].toString())); } } resetQuery(); applySort(this.query, result); applyCriteriaList(this.query, appliedCriteria); this.sort = result; refresh(); fireItemSetChange(); } protected Query applySort(Query q, Sort s) { q.with(s); return q; } protected Query applyCriteriaList(Query q, List<Criteria> criteriaList) { for (Criteria c : criteriaList) q.addCriteria(c); return q; } @Override public Collection<?> getSortableContainerPropertyIds() { return getContainerPropertyIds(); } }