/*
* Copyright 2015, The Querydsl Team (http://www.querydsl.com/team)
*
* 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.querydsl.core.types;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.primitives.Primitives;
import com.querydsl.core.group.GroupExpression;
/**
* {@code QBean} is a JavaBean populating projection type
*
* <p>Example</p>
*
* <pre>
* {@code
* QEmployee employee = QEmployee.employee;
* List<EmployeeInfo> result = query.from(employee)
* .where(employee.valid.eq(true))
* .select(Projections.bean(EmployeeInfo.class, employee.firstName, employee.lastName))
* .fetch();
* }
* </pre>
*
* @author tiwe
*
* @param <T> bean type
*/
public class QBean<T> extends FactoryExpressionBase<T> {
private static final long serialVersionUID = -8210214512730989778L;
private static ImmutableMap<String,Expression<?>> createBindings(Expression<?>... args) {
ImmutableMap.Builder<String, Expression<?>> rv = ImmutableMap.builder();
for (Expression<?> expr : args) {
if (expr instanceof Path<?>) {
Path<?> path = (Path<?>) expr;
rv.put(path.getMetadata().getName(), expr);
} else if (expr instanceof Operation<?>) {
Operation<?> operation = (Operation<?>) expr;
if (operation.getOperator() == Ops.ALIAS && operation.getArg(1) instanceof Path<?>) {
Path<?> path = (Path<?>) operation.getArg(1);
if (isCompoundExpression(operation.getArg(0))) {
rv.put(path.getMetadata().getName(), operation.getArg(0));
} else {
rv.put(path.getMetadata().getName(), operation);
}
} else {
throw new IllegalArgumentException("Unsupported expression " + expr);
}
} else {
throw new IllegalArgumentException("Unsupported expression " + expr);
}
}
return rv.build();
}
private static boolean isCompoundExpression(Expression<?> expr) {
return expr instanceof FactoryExpression || expr instanceof GroupExpression;
}
private static Class<?> normalize(Class<?> cl) {
return cl.isPrimitive() ? Primitives.wrap(cl) : cl;
}
private static boolean isAssignableFrom(Class<?> cl1, Class<?> cl2) {
return normalize(cl1).isAssignableFrom(normalize(cl2));
}
private final ImmutableMap<String, Expression<?>> bindings;
private final List<Field> fields;
private final List<Method> setters;
private final boolean fieldAccess;
/**
* Create a new QBean instance
*
* @param type type of bean
* @param bindings bindings
*/
protected QBean(Class<? extends T> type, Map<String, ? extends Expression<?>> bindings) {
this(type, false, bindings);
}
/**
* Create a new QBean instance
*
* @param type type of bean
* @param args properties to be populated
*/
protected QBean(Class<? extends T> type, Expression<?>... args) {
this(type, false, args);
}
/**
* Create a new QBean instance
*
* @param type type of bean
* @param fieldAccess true, for field access and false, for property access
* @param args fields or properties to be populated
*/
protected QBean(Class<? extends T> type, boolean fieldAccess, Expression<?>... args) {
this(type, fieldAccess, createBindings(args));
}
/**
* Create a new QBean instance
*
* @param type type of bean
* @param fieldAccess true, for field access and false, for property access
* @param bindings bindings
*/
protected QBean(Class<? extends T> type, boolean fieldAccess, Map<String, ? extends Expression<?>> bindings) {
super(type);
this.bindings = ImmutableMap.copyOf(bindings);
this.fieldAccess = fieldAccess;
if (fieldAccess) {
this.fields = initFields(bindings);
this.setters = ImmutableList.of();
} else {
this.fields = ImmutableList.of();
this.setters = initMethods(bindings);
}
}
private List<Field> initFields(Map<String, ? extends Expression<?>> args) {
List<Field> fields = new ArrayList<Field>(args.size());
for (Map.Entry<String,? extends Expression<?>> entry : args.entrySet()) {
String property = entry.getKey();
Expression<?> expr = entry.getValue();
Class<?> beanType = getType();
Field field = null;
while (!beanType.equals(Object.class)) {
try {
field = beanType.getDeclaredField(property);
field.setAccessible(true);
if (!isAssignableFrom(field.getType(), expr.getType())) {
typeMismatch(field.getType(), expr);
}
beanType = Object.class;
} catch (SecurityException e) {
// do nothing
} catch (NoSuchFieldException e) {
beanType = beanType.getSuperclass();
}
}
if (field == null) {
propertyNotFound(expr, property);
}
fields.add(field);
}
return fields;
}
private List<Method> initMethods(Map<String, ? extends Expression<?>> args) {
try {
List<Method> methods = new ArrayList<Method>(args.size());
BeanInfo beanInfo = Introspector.getBeanInfo(getType());
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
for (Map.Entry<String, ? extends Expression<?>> entry : args.entrySet()) {
String property = entry.getKey();
Expression<?> expr = entry.getValue();
Method setter = null;
for (PropertyDescriptor prop : propertyDescriptors) {
if (prop.getName().equals(property)) {
setter = prop.getWriteMethod();
if (!isAssignableFrom(prop.getPropertyType(), expr.getType())) {
typeMismatch(prop.getPropertyType(), expr);
}
break;
}
}
if (setter == null) {
propertyNotFound(expr, property);
}
methods.add(setter);
}
return methods;
} catch (IntrospectionException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
protected void propertyNotFound(Expression<?> expr, String property) {
// do nothing
}
protected void typeMismatch(Class<?> type, Expression<?> expr) {
final String msg = expr.getType().getName() + " is not compatible with " + type.getName();
throw new IllegalArgumentException(msg);
}
@Override
public T newInstance(Object... a) {
try {
T rv = create(getType());
if (fieldAccess) {
for (int i = 0; i < a.length; i++) {
Object value = a[i];
if (value != null) {
Field field = fields.get(i);
if (field != null) {
field.set(rv, value);
}
}
}
} else {
for (int i = 0; i < a.length; i++) {
Object value = a[i];
if (value != null) {
Method setter = setters.get(i);
if (setter != null) {
setter.invoke(rv, value);
}
}
}
}
return rv;
} catch (InstantiationException e) {
throw new ExpressionException(e.getMessage(), e);
} catch (IllegalAccessException e) {
throw new ExpressionException(e.getMessage(), e);
} catch (InvocationTargetException e) {
throw new ExpressionException(e.getMessage(), e);
}
}
protected <T> T create(Class<T> type) throws IllegalAccessException, InstantiationException {
return type.newInstance();
}
/**
* Create an alias for the expression
*
* @return this as alias
*/
@SuppressWarnings("unchecked")
public Expression<T> as(Path<T> alias) {
return ExpressionUtils.operation(getType(),Ops.ALIAS, this, alias);
}
/**
* Create an alias for the expression
*
* @return this as alias
*/
public Expression<T> as(String alias) {
return as(ExpressionUtils.path(getType(), alias));
}
@Override
public <R,C> R accept(Visitor<R,C> v, C context) {
return v.visit(this, context);
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
} else if (obj instanceof QBean<?>) {
QBean<?> c = (QBean<?>) obj;
return getArgs().equals(c.getArgs()) && getType().equals(c.getType());
} else {
return false;
}
}
@Override
public List<Expression<?>> getArgs() {
return bindings.values().asList();
}
}