Java tutorial
/** * Copyright (C) 2013 cherimojava (http://github.com/cherimojava/cherimodata) 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 com.github.cherimojava.data.mongo.query; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.List; import org.bson.conversions.Bson; import com.github.cherimojava.data.mongo.entity.Entity; import com.github.cherimojava.data.mongo.entity.EntityFactory; import com.github.cherimojava.data.mongo.entity.EntityProperties; import com.github.cherimojava.data.mongo.entity.ParameterProperty; import com.google.common.base.Defaults; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.Lists; import com.google.common.primitives.Primitives; import com.mongodb.client.FindIterable; import com.mongodb.client.MongoCollection; import com.mongodb.client.model.Filters; import com.mongodb.client.model.Sorts; /** * Outer invocation Handler joining all parts of the query building together like: * * <pre> * Query q = QueryBuilder.query(Entity.class); * q.where(q.e().getProperty()).is(something).and(q.e().getProperty2()).between(val1,val2).orderBy(desc(getDesc()),asc(getAsc()).limit(x).skip(y).getAll() * </pre> */ public class QueryInvocationHandler implements InvocationHandler { private final MongoCollection<? extends Entity> coll; Class<? extends Entity> clazz; Supplier<Entity> entityProxy; Supplier<QuerySpecifier> specifier; Supplier<QueryEnd> queryEnd; Supplier<QuerySort> querySort; OngoingQuery proxy; EntityProperties properties; List<ParameterProperty> curQueriedProperty = Lists.newArrayList(); List<Bson> filters = Lists.newArrayList(); Integer limit = null; Integer skip = null; List<Bson> sorts = Lists.newArrayList(); List<String> curSorts = Lists.newArrayList(); private boolean sortSet = false; public ParameterProperty getProperty(Method m) { return properties.getProperty(m); } public void addCurQueriedProperty(ParameterProperty curQueriedProperty) { this.curQueriedProperty.add(curQueriedProperty); } /** * Creates a new Invocation handler for the given Entity class and collection, along with the related * EntityProperties * * @param clazz Entity class the query is based for * @param coll Collection against which the query will be performed * @param properties Entities properties of the entity class * @param <E> */ public <E extends Entity> QueryInvocationHandler(Class<E> clazz, MongoCollection<E> coll, EntityProperties properties) { this.coll = coll; this.properties = properties; this.clazz = clazz; entityProxy = Suppliers.memoize(() -> (Entity) Proxy.newProxyInstance(getClass().getClassLoader(), new Class[] { clazz }, new EntityQueryInvocationHandler(this))); specifier = Suppliers.memoize(() -> (QuerySpecifier) Proxy.newProxyInstance(getClass().getClassLoader(), new Class[] { QuerySpecifier.class }, new QuerySpecifierInvocationHandler(this))); queryEnd = Suppliers.memoize(() -> (QueryEnd) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[] { QueryEnd.class }, this)); querySort = Suppliers.memoize(() -> (QuerySort) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[] { QuerySort.class }, this)); } /** * sets the proxy this QueryInvocationHandler represents * * @param proxy this QueryInvocationHandler is representing */ public void setProxy(OngoingQuery proxy) { this.proxy = proxy; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); switch (methodName) { case "e": // this is just a handout for the entity to get knowledge of what to check return this.entityProxy.get(); case "where":/* fallthrough */ case "and": return this.specifier.get(); case "iterator": FindIterable it = coll.find(Filters.and(filters.toArray(new Bson[] {}))); if (limit != null) { it.limit(limit); } if (skip != null) { it.skip(skip); } if (sorts.size() > 0) { it.sort(Sorts.orderBy(sorts)); } return it.iterator(); case "count": return coll.count(Filters.and(filters.toArray(new Bson[] {}))); case "limit": limit = (Integer) args[0]; return queryEnd.get(); case "skip": skip = (Integer) args[0]; return queryEnd.get(); case "sort": checkState(!sortSet, "Sorting can be specified only once"); sortSet = true; return querySort.get(); case "desc":/* fallthrough */ case "asc": return addSortInformation("asc".equals(methodName)); case "by": return addSortInformation(args[0] == QuerySort.Sort.ASC); } throw new IllegalStateException("Unknown method found: " + methodName); } private QuerySort addSortInformation(boolean asc) { curQueriedProperty.forEach(parameterProperty -> curSorts.add(parameterProperty.getMongoName())); if (asc) { sorts.add(Sorts.ascending(curSorts)); } else { sorts.add(Sorts.descending(curSorts)); } curSorts.clear(); curQueriedProperty.clear(); return querySort.get(); } /** * retrieves the mongodb name currently queried against and clears it to avoid illegal states * * @return mongodb name queried against */ private String getCurrentMongoName() { checkState(curQueriedProperty.size() == 1, "can't get mongo name from null property"); String name = curQueriedProperty.get(0).getMongoName(); // after this was set change back to null, as this invocation is exhausted curQueriedProperty.clear(); return name; } /** * InvocationHandler capturing which property is about to be queried */ private class EntityQueryInvocationHandler implements InvocationHandler { private final QueryInvocationHandler parent; EntityQueryInvocationHandler(QueryInvocationHandler parent) { this.parent = parent; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { ParameterProperty property = getProperty(method); if (Entity.class.isAssignableFrom(method.getReturnType())) { checkArgument(!property.isDBRef(), "nested search is currently not working on DBRef-References."); // the remembering is done nested, first remember the parent element parent.curQueriedProperty.add(property); return Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[] { method.getReturnType() }, new InnerEntityQueryIdOnlyInvocationHandler( EntityFactory.getProperties((Class<? extends Entity>) method.getReturnType()))); } else { // remember which property was invoked so we can build the query condition based on it parent.addCurQueriedProperty(parent.getProperty(method)); if (property.isPrimitiveType()) { return Defaults.defaultValue(Primitives.unwrap(property.getType())); } else { return null; } } } /** * InvocationHandler ensuring that only on Id fields a query can be performed */ private class InnerEntityQueryIdOnlyInvocationHandler implements InvocationHandler { private final EntityProperties properties; public InnerEntityQueryIdOnlyInvocationHandler(EntityProperties properties) { this.properties = properties; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { ParameterProperty curProperty = properties.getProperty(method); if (properties.getIdProperty() == curProperty) { // for primitives we need to return something, so return default if (curProperty.isPrimitiveType()) { return Defaults.defaultValue(Primitives.unwrap(curProperty.getType())); } else { return null; } } else { throw new IllegalArgumentException( "Only can perform nested query on id field, but was " + curProperty); } } } } /** * InvocationHandler creating the query condition */ private class QuerySpecifierInvocationHandler implements InvocationHandler { private final QueryInvocationHandler parent; QuerySpecifierInvocationHandler(QueryInvocationHandler parent) { this.parent = parent; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // depending on the method add given filters to the query String property = getCurrentMongoName(); switch (method.getName()) { case "is": filters.add(Filters.eq(property, enumToString(args[0]))); break; case "between": filters.add(Filters.gte(property, args[0])); filters.add(Filters.lte(property, args[1])); break; case "lessThan": filters.add(Filters.lt(property, args[0])); break; case "lessThanEqual": filters.add(Filters.lte(property, args[0])); break; case "greaterThan": filters.add(Filters.gt(property, args[0])); break; case "greaterThanEqual": filters.add(Filters.gte(property, args[0])); break; case "in": filters.add(Filters.in(property, enumsToString((Object[]) args[0]))); break; } return parent.proxy; } /** * converts an enum into its String representation and keeps all other objects as is. * * @param obj pontential enum to convert into it's String representation * @return unmodified obj if the obj is no enum, or the String representation of the enum otherwise */ private Object enumToString(Object obj) { if (obj != null && obj.getClass().isEnum()) { return obj.toString(); } else { return obj; } } /** * converts an enum array into their String representation and keeps all other objects as is. * * @param objs potential enum array to convert into it's String representation * @return unmodified obj array if the array is no enum array, or the String representation of the enums * otherwise */ private Object[] enumsToString(Object... objs) { if (objs != null && objs.getClass().getComponentType().isEnum()) { String[] converted = new String[objs.length]; int i = 0; for (Object o : objs) { converted[i++] = o.toString(); } return converted; } else { return objs; } } } }