blob: 813556fb30d7887f7d81e8ac36a0e148da25f83d [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.openjpa.kernel;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.openjpa.lib.util.J2DoPrivHelper;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.StringUtil;
import org.apache.openjpa.util.OpenJPAException;
import org.apache.openjpa.util.UserException;
/**
* Helper class to pack results into the result class set on the query.
*
* @author Abe White
* @author Patrick Linskey
*/
public class ResultPacker {
private static final Localizer _loc = Localizer.forPackage
(ResultPacker.class);
private static final Set<Class<?>> _stdTypes = new HashSet<>();
static {
_stdTypes.add(Object[].class);
_stdTypes.add(Object.class);
_stdTypes.add(Map.class);
_stdTypes.add(HashMap.class);
_stdTypes.add(Character.class);
_stdTypes.add(Boolean.class);
_stdTypes.add(Byte.class);
_stdTypes.add(Short.class);
_stdTypes.add(Integer.class);
_stdTypes.add(Long.class);
_stdTypes.add(Float.class);
_stdTypes.add(Double.class);
_stdTypes.add(String.class);
_stdTypes.add(BigInteger.class);
_stdTypes.add(BigDecimal.class);
_stdTypes.add(Date.class);
_stdTypes.add(java.sql.Date.class);
_stdTypes.add(java.sql.Time.class);
_stdTypes.add(java.sql.Timestamp.class);
_stdTypes.add(Calendar.class);
_stdTypes.add(GregorianCalendar.class);
}
private final Class<?> _resultClass;
private final String[] _aliases;
private final Member[] _sets;
private final Method _put;
private final Constructor<?> _constructor;
/**
* Protected constructor to bypass this implementation but allow extension.
*/
protected ResultPacker() {
_resultClass = null;
_aliases = null;
_sets = null;
_put = null;
_constructor = null;
}
/**
* Constructor for result class without a projection.
*/
public ResultPacker(Class<?> candidate, String alias, Class<?> resultClass) {
this(candidate, null, new String[]{ alias }, resultClass);
}
/**
* Constructor for standard projection.
*
* @param types the projection value types
* @param aliases the alias for each projection value
* @param resultClass the class to pack into
*/
public ResultPacker(Class<?>[] types, String[] aliases, Class<?> resultClass) {
this(null, types, aliases, resultClass);
}
/**
* Internal constructor.
*/
private ResultPacker(Class<?> candidate, Class<?>[] types, String[] aliases, Class<?> resultClass) {
_aliases = aliases;
if (candidate == resultClass || isInterface(resultClass, candidate)
||(types != null && types.length == 1 && types[0] == resultClass)
|| resultClass.isArray()) {
_resultClass = resultClass;
_sets = null;
_put = null;
_constructor = null;
} else if (resultClass.isPrimitive()) {
assertConvertable(candidate, types, resultClass);
_resultClass = Filters.wrap(resultClass);
_sets = null;
_put = null;
_constructor = null;
} else if (!_stdTypes.contains(_resultClass = resultClass)) {
// check for a constructor that matches the projection types
Constructor<?> cons = null;
if (types != null && types.length > 0) {
try {
cons = _resultClass.getConstructor(types);
} catch (NoSuchMethodException nsme) {
}
}
_constructor = cons;
if (cons == null) {
Method[] methods = _resultClass.getMethods();
Field[] fields = _resultClass.getFields();
_put = findPut(methods);
_sets = new Member[aliases.length];
Class<?> type;
for (int i = 0; i < _sets.length; i++) {
type = (types == null) ? candidate : types[i];
_sets[i] = findSet(aliases[i], type, fields, methods);
if (_sets[i] == null && _put == null)
throw new UserException(_loc.get("cant-set",
resultClass, aliases[i],
types == null ? null : Arrays.asList(types)));
}
} else {
_sets = null;
_put = null;
}
} else {
if (resultClass != Map.class && resultClass != HashMap.class
&& resultClass != Object[].class)
assertConvertable(candidate, types, resultClass);
_sets = null;
_put = null;
_constructor = null;
}
}
boolean isInterface(Class<?> intf, Class<?> actual) {
if (actual != null) {
Class<?>[] intfs = actual.getInterfaces();
for (Class<?> c : intfs) {
if (c == intf)
return true;
}
}
return false;
}
/**
* Ensure that conversion is possible.
*/
private void assertConvertable(Class<?> candidate, Class<?>[] types,
Class<?> resultClass) {
Class<?> c = (types == null) ? candidate : types[0];
if ((types != null && types.length != 1) || (c != null
&& c != Object.class && !Filters.canConvert(c, resultClass, true)))
throw new UserException(_loc.get("cant-convert-result",
c, resultClass));
}
/**
* Pack the given object into an instance of the query's result class.
*/
public Object pack(Object result) {
if (result == null || _resultClass == result.getClass())
return result;
// special cases for efficient basic types where we want to avoid
// creating an array for call to general pack method below
if (_resultClass == Object.class)
return result;
if (_resultClass == Object[].class)
return new Object[]{ result };
if (_resultClass == HashMap.class || _resultClass == Map.class) {
HashMap<String,Object> map = new HashMap<>(1, 1F);
map.put(_aliases[0], result);
return map;
}
// primitive or simple type?
if (_constructor == null && _sets == null)
return Filters.convert(result, _resultClass);
// this is some complex case, so worth it to create the array and
// use the general pack method
return packUserType(new Object[]{ result });
}
/**
* Pack the given array into an instance of the query's result class.
*/
public Object pack(Object[] result) {
if (result == null || result.length == 0)
return null;
// special cases for object arrays and maps
if (_resultClass == Object[].class) {
// the result might contain extra data at the end
if (result.length > _aliases.length) {
Object[] trim = new Object[_aliases.length];
System.arraycopy(result, 0, trim, 0, trim.length);
return trim;
}
return result;
}
if (_resultClass.isArray()) {
Class<?> elementType = _resultClass.getComponentType();
Object castResult = Array.newInstance(elementType, result.length);
for (int i = 0; i < result.length; i++)
Array.set(castResult, i, elementType.cast(result[i]));
return castResult;
}
if (_resultClass == Object.class)
return result[0];
if (_resultClass == HashMap.class || _resultClass == Map.class) {
Map<String,Object> map = new HashMap<>(result.length);
for (int i = 0; i < _aliases.length; i++)
map.put(_aliases[i], result[i]);
return map;
}
// primitive or simple type?
if (_sets == null && _constructor == null)
return Filters.convert(result[0], _resultClass);
// must be a user-defined type
return packUserType(result);
}
/**
* Pack the given result into the user-defined result class.
*/
private Object packUserType(Object[] result) {
try {
// use the constructor first, if we have one
if (_constructor != null)
return _constructor.newInstance(result);
Object user = AccessController.doPrivileged(
J2DoPrivHelper.newInstanceAction(_resultClass));
for (int i = 0; i < _aliases.length; i++) {
if (_sets[i] instanceof Method) {
Method meth = (Method) _sets[i];
meth.invoke(user, new Object[]{ Filters.convert
(result[i], meth.getParameterTypes()[0]) });
} else if (_sets[i] instanceof Field) {
Field field = (Field) _sets[i];
field.set(user, Filters.convert(result[i],
field.getType()));
} else if (_put != null) {
_put.invoke(user, new Object[]{ _aliases[i], result[i] });
}
}
return user;
} catch (OpenJPAException ke) {
throw ke;
} catch (PrivilegedActionException pae) {
throw new UserException(_loc.get("pack-instantiation-err",
_resultClass), pae.getException());
} catch (InstantiationException ie) {
throw new UserException(_loc.get("pack-instantiation-err",
_resultClass), ie);
} catch (Exception e) {
throw new UserException(_loc.get("pack-err", _resultClass), e);
}
}
/**
* Return the set method for the given property.
*/
private static Member findSet(String alias, Class<?> type, Field[] fields,
Method[] methods) {
if (StringUtil.isEmpty(alias))
return null;
if (type == Object.class)
type = null;
// check public fields first
Field field = null;
for (Field item : fields) {
// if we find a field with the exact name, either return it
// if it's the right type or give up if it's not
if (item.getName().equals(alias)) {
if (type == null
|| Filters.canConvert(type, item.getType(), true))
return item;
break;
}
// otherwise if we find a field with the right name but the
// wrong case, record it and if we don't find an exact match
// for a field or setter we'll use it
if (field == null && item.getName().equalsIgnoreCase(alias)
&& (type == null
|| Filters.canConvert(type, item.getType(), true)))
field = item;
}
// check setter methods
String setName = "set" + StringUtil.capitalize(alias);
Method method = null;
boolean eqName = false;
Class<?>[] params;
for (Method value : methods) {
if (!value.getName().equalsIgnoreCase(setName))
continue;
params = value.getParameterTypes();
if (params.length != 1)
continue;
if (type != null && params[0] == Object.class) {
// we found a generic object setter; now see if the name
// is an exact match, and if so record this setter. if we
// don't find an exact type match later, we'll use it. if
// the names are not an exact match, only record this setter
// if we haven't found any others that match at all
if (value.getName().equals(setName)) {
eqName = true;
method = value;
}
else if (method == null)
method = value;
}
else if (type == null || Filters.canConvert(type, params[0], true)) {
// we found a setter with the right type; now see if the name
// is an exact match. if so, return the setter. if not,
// record the setter only if we haven't found a generic one
// with an exact name match
if (value.getName().equals(setName))
return value;
if (method == null || !eqName)
method = value;
}
}
// if we have an exact method name match, return it; otherwise favor
// an inexact field to an inexact method
if (eqName || field == null)
return method;
return field;
}
/**
* Return the put method if one exists.
*/
private static Method findPut(Method[] methods) {
Class<?>[] params;
for (Method method : methods) {
if (!method.getName().equals("put"))
continue;
params = method.getParameterTypes();
if (params.length == 2
&& params[0] == Object.class
&& params[1] == Object.class)
return method;
}
return null;
}
}