/* * Copyright 2009 Alberto Gimeno <gimenete at gmail.com> * * 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 siena; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import siena.core.Aggregated; import siena.core.Aggregator; import siena.core.InheritFilter; import siena.core.Many; import siena.core.One; import siena.core.Owned; import siena.core.Owner; import siena.core.Relation; import siena.core.RelationMode; import siena.core.lifecycle.LifeCyclePhase; import siena.core.lifecycle.LifeCycleUtils; import siena.embed.Embedded; public class ClassInfo { protected static Map<Class<?>, ClassInfo> infoClasses = new ConcurrentHashMap<Class<?>, ClassInfo>(); public Class<?> clazz; public String tableName; public List<Field> keys = new ArrayList<Field>(); public List<Field> insertFields = new ArrayList<Field>(); public List<Field> updateFields = new ArrayList<Field>(); public List<Field> generatedKeys = new ArrayList<Field>(); public List<Field> allFields = new ArrayList<Field>(); public List<Field> joinFields = new ArrayList<Field>(); public List<Field> allExtendedFields = new ArrayList<Field>(); public List<Field> aggregatedFields = new ArrayList<Field>(); public boolean hasAggregatedFields = false; public List<Field> ownedFields = new ArrayList<Field>(); public boolean hasOwnedFields = false; public Map<LifeCyclePhase, List<Method>> lifecycleMethods = new HashMap<LifeCyclePhase, List<Method>>(); public Map<Field, Map<FieldMapKeys, Object>> queryFieldMap = new HashMap<Field, Map<FieldMapKeys, Object>>(); public Map<Field, Map<FieldMapKeys, Object>> manyFieldMap = new HashMap<Field, Map<FieldMapKeys, Object>>(); public Map<Field, Map<FieldMapKeys, Object>> oneFieldMap = new HashMap<Field, Map<FieldMapKeys, Object>>(); // this aggregator field is the field identified as containing the aggregator // there can be only ONE aggregator in a class public Field aggregator = null; public boolean hasAggregator = false; public enum FieldMapKeys { CLASS, MODE, FIELD, FILTER } protected ClassInfo(Class<?> clazz) { this.clazz = clazz; tableName = getTableName(clazz); // Takes into account superclass fields for inheritance!!!! List<Class<?>> classH = new ArrayList<Class<?>>(); Class<?> cl = clazz; Set<String> removedFields = new HashSet<String>(); scanClassHierarchy(cl, classH, removedFields); for(Class<?> c: classH) { for (Field field : c.getDeclaredFields()) { if(removedFields.contains(field.getName())) continue; Class<?> type = field.getType(); if(shouldSkip(field)){ continue; } if(isId(field)){ buildId(field); continue; } else if(type == Query.class){ buildQuery(field, c); // QUERY fields are not added to other kind of fields continue; } else if(type == Many.class){ buildMany(field, c); // MANY fields are not added to other kind of fields continue; } else if(type == One.class){ buildOne(field, c); // ONE fields are not added to other kind of fields continue; } else if(isAggregator(field)){ // only one @Aggregator per model outside the one from Model if(aggregator != null){ if(c != Model.class){ throw new SienaException("Found 2 @Aggregator fields in your model which is forbidden"); } } else { if(type != Relation.class){ throw new SienaException("Found @Aggregator field not with type siena.core.Relation which is forbidden"); } aggregator = field; hasAggregator = true; } continue; } validateAnnotations(field, type); // add other fields updateFields.add(field); insertFields.add(field); allFields.add(field); allExtendedFields.add(field); } buildLifecycleMethods(c); } } private void buildId(Field field){ Class<?> type = field.getType(); Id id = field.getAnnotation(Id.class); if(id != null) { // ONLY long ID can be auto_incremented if(id.value() == Generator.AUTO_INCREMENT && ( Long.TYPE == type || Long.class.isAssignableFrom(type))) { generatedKeys.add(field); } else { insertFields.add(field); } keys.add(field); allFields.add(field); allExtendedFields.add(field); } } private void buildQuery(Field field, Class<?> c) { Class<?> type = field.getType(); Filter filter = field.getAnnotation(Filter.class); Owned related = field.getAnnotation(Owned.class); if(filter == null && related == null ) { throw new SienaException("Found Query<T> field without @Filter or @Owned annotation at " +c.getName()+"."+field.getName()); } ParameterizedType pt = (ParameterizedType) field.getGenericType(); Class<?> cl = (Class<?>) pt.getActualTypeArguments()[0]; if(filter != null){ try { Map<FieldMapKeys, Object> fieldMap = new HashMap<FieldMapKeys, Object>(); fieldMap.put(FieldMapKeys.CLASS, cl); fieldMap.put(FieldMapKeys.FILTER, filter.value()); queryFieldMap.put(field, fieldMap); ownedFields.add(field); hasOwnedFields = true; } catch (Exception e) { throw new SienaException(e); } } else if(related != null){ String as = related.mappedBy(); // if related.as not specified, tries to find the first field with this type if("".equals(as) || as == null){ ClassInfo fieldInfo = ClassInfo.getClassInfo(cl); Field f = fieldInfo.getFirstFieldFromType(clazz); if(f == null){ throw new SienaException("@Owned without 'as' attribute and no field of type " + clazz.getName() + "found in class "+type.getName()); } as = ClassInfo.getSimplestColumnName(f); } try { Map<FieldMapKeys, Object> fieldMap = new HashMap<FieldMapKeys, Object>(); fieldMap.put(FieldMapKeys.CLASS, cl); fieldMap.put(FieldMapKeys.FILTER, as); queryFieldMap.put(field, fieldMap); ownedFields.add(field); hasOwnedFields = true; } catch (Exception e) { throw new SienaException(e); } } allExtendedFields.add(field); } private void buildMany(Field field, Class<?> c) { Class<?> type = field.getType(); ParameterizedType pt = (ParameterizedType) field.getGenericType(); Class<?> cl = (Class<?>) pt.getActualTypeArguments()[0]; Aggregated agg = field.getAnnotation(Aggregated.class); Filter filter = field.getAnnotation(Filter.class); Owned related = field.getAnnotation(Owned.class); if((agg!=null && filter!=null) || (agg!=null && related!=null)){ throw new SienaException("Found Many<T> field " + c.getName()+"."+field.getName() + "with @Filter+@Owned or @Filter+@Owned: this is not authorized"); } if(agg != null){ try { Map<FieldMapKeys, Object> fieldMap = new HashMap<FieldMapKeys, Object>(); fieldMap.put(FieldMapKeys.CLASS, cl); fieldMap.put(FieldMapKeys.MODE, RelationMode.AGGREGATION); manyFieldMap.put(field, fieldMap); aggregatedFields.add(field); hasAggregatedFields = true; } catch (Exception e) { throw new SienaException(e); } }else if(filter != null){ try { Field filterField = cl.getField(filter.value()); if(filterField == null){ throw new SienaException("@Filter error: Couldn't find field " + filter.value() + "in class "+cl.getName()); } Map<FieldMapKeys, Object> fieldMap = new HashMap<FieldMapKeys, Object>(); fieldMap.put(FieldMapKeys.CLASS, cl); fieldMap.put(FieldMapKeys.MODE, RelationMode.RELATION); fieldMap.put(FieldMapKeys.FIELD, filterField); fieldMap.put(FieldMapKeys.FILTER, filter.value()); manyFieldMap.put(field, fieldMap); ownedFields.add(field); hasOwnedFields = true; } catch (Exception e) { throw new SienaException(e); } }else if(related != null) { String as = related.mappedBy(); // if related.as not specified, tries to find the first field with this type if("".equals(as) || as == null){ ClassInfo fieldInfo = ClassInfo.getClassInfo(cl); Field f = fieldInfo.getFirstFieldFromType(clazz); if(f == null){ throw new SienaException("@Owned without 'as' attribute and no field of type " + clazz.getName() + "found in class "+type.getName()); } as = ClassInfo.getSimplestColumnName(f); } try { Field asField = cl.getField(as); if(asField == null){ throw new SienaException("@Filter error: Couldn't find field " + as + "in class "+cl.getName()); } Map<FieldMapKeys, Object> fieldMap = new HashMap<FieldMapKeys, Object>(); fieldMap.put(FieldMapKeys.CLASS, cl); fieldMap.put(FieldMapKeys.MODE, RelationMode.RELATION); fieldMap.put(FieldMapKeys.FIELD, asField); fieldMap.put(FieldMapKeys.FILTER, as); manyFieldMap.put(field, fieldMap); ownedFields.add(field); hasOwnedFields = true; } catch (Exception e) { throw new SienaException(e); } } allExtendedFields.add(field); } private void buildOne(Field field, Class<?> c) { Class<?> type = field.getType(); ParameterizedType pt = (ParameterizedType) field.getGenericType(); Class<?> cl = (Class<?>) pt.getActualTypeArguments()[0]; Aggregated agg = field.getAnnotation(Aggregated.class); Filter filter = field.getAnnotation(Filter.class); Owned related = field.getAnnotation(Owned.class); if((agg!=null && filter!=null) || (agg!=null && related!=null)){ throw new SienaException("Found One<T> field " + c.getName()+"."+field.getName() + "with @Filter+@Aggregated or @Filter+@Owned: this is not authorized"); } if(agg != null){ try { Map<FieldMapKeys, Object> fieldMap = new HashMap<FieldMapKeys, Object>(); fieldMap.put(FieldMapKeys.CLASS, cl); fieldMap.put(FieldMapKeys.MODE, RelationMode.AGGREGATION); oneFieldMap.put(field, fieldMap); aggregatedFields.add(field); hasAggregatedFields = true; } catch (Exception e) { throw new SienaException(e); } }else if(filter != null){ try { Field filterField = cl.getField(filter.value()); if(filterField == null){ throw new SienaException("@Filter error: Couldn't find field " + filter.value() + "in class "+cl.getName()); } Map<FieldMapKeys, Object> fieldMap = new HashMap<FieldMapKeys, Object>(); fieldMap.put(FieldMapKeys.CLASS, cl); fieldMap.put(FieldMapKeys.MODE, RelationMode.RELATION); fieldMap.put(FieldMapKeys.FIELD, filterField); fieldMap.put(FieldMapKeys.FILTER, filter.value()); oneFieldMap.put(field, fieldMap); ownedFields.add(field); hasOwnedFields = true; } catch (Exception e) { throw new SienaException(e); } }else if(related != null) { String as = related.mappedBy(); // if related.as not specified, tries to find the first field with this type if("".equals(as) || as == null){ ClassInfo fieldInfo = ClassInfo.getClassInfo(cl); Field f = fieldInfo.getFirstFieldFromType(clazz); if(f == null){ throw new SienaException("@Owned without 'as' attribute and no field of type " + clazz.getName() + "found in class "+type.getName()); } as = ClassInfo.getSimplestColumnName(f); } try { Field asField = cl.getField(as); if(asField == null){ throw new SienaException("@Filter error: Couldn't find field " + as + "in class "+cl.getName()); } Map<FieldMapKeys, Object> fieldMap = new HashMap<FieldMapKeys, Object>(); fieldMap.put(FieldMapKeys.CLASS, cl); fieldMap.put(FieldMapKeys.MODE, RelationMode.RELATION); fieldMap.put(FieldMapKeys.FIELD, asField); fieldMap.put(FieldMapKeys.FILTER, as); oneFieldMap.put(field, fieldMap); ownedFields.add(field); hasOwnedFields = true; } catch (Exception e) { throw new SienaException(e); } } allExtendedFields.add(field); } private void buildLifecycleMethods(Class<?> c) { for(Method m : c.getDeclaredMethods()){ List<LifeCyclePhase> lcps = LifeCycleUtils.getMethodLifeCycles(m); for(LifeCyclePhase lcp: lcps){ List<Method> methods = lifecycleMethods.get(lcp); if(methods == null){ methods = new ArrayList<Method>(); lifecycleMethods.put(lcp, methods); } methods.add(m); } } } private static void scanClassHierarchy(Class<?> cl, List<Class<?>> classH, Set<String> removedFields) { while (cl!=null) { classH.add(0, cl); // add exceptFields InheritFilter iFilter = cl.getAnnotation(InheritFilter.class); if(iFilter != null){ String[] efs = iFilter.removedFields(); for(String ef:efs){ removedFields.add(ef); } } cl = cl.getSuperclass(); } } private static boolean scanIdInHierarchy(Class<?> clazz){ // Takes into account superclass fields for inheritance!!!! List<Class<?>> classH = new ArrayList<Class<?>>(); Class<?> cl = clazz; Set<String> removedFields = new HashSet<String>(); scanClassHierarchy(cl, classH, removedFields); for(Class<?> c: classH) { for (Field field : c.getDeclaredFields()) { if(removedFields.contains(field.getName())) continue; if(shouldSkip(field)){ continue; } if(isId(field)){ return true; } } } return false; } private void validateAnnotations(Field field, Class<?> type) { if(ClassInfo.isModel(type)){ if(isJoined(field)){ joinFields.add(field); }else if(isOwned(field)){ throw new SienaException("@Owned not possible on Field '"+field.getName()+"' without @One/@Many"); }else if(isAggregated(field)){ throw new SienaException("@Aggregated not possible on Field '"+field.getName()+"' without @One/@Many"); } }else { if(isJoined(field)){ throw new SienaException("@Join not possible: Field "+field.getName()+" is not a relation field"); }else if(isOwned(field)){ throw new SienaException("@Owned not possible: Field "+field.getName()+" is not a relation field"); }else if(isAggregated(field)){ throw new SienaException("@Aggregated not possible: Field "+field.getName()+" is not a relation field"); } } } private static boolean shouldSkip(Field field){ int modifiers = field.getModifiers(); if((modifiers & Modifier.TRANSIENT) == Modifier.TRANSIENT || (modifiers & Modifier.STATIC) == Modifier.STATIC || field.isSynthetic() || field.getType() == Class.class || field.getAnnotation(Ignore.class) != null) return true; return false; } private String getTableName(Class<?> clazz) { Table t = clazz.getAnnotation(Table.class); if(t == null) return clazz.getSimpleName(); return t.value(); } public List<String> getUpdateFieldsColumnNames() { List<String> strs = new ArrayList<String>(this.updateFields.size()); for(Field field: this.updateFields){ Column c = field.getAnnotation(Column.class); if(c != null && c.value().length > 0) { strs.add(c.value()[0]); } // default mapping: field names else if(isModel(field.getType())) { ClassInfo ci = getClassInfo(field.getType()); for (Field key : ci.keys) { Collections.addAll(strs, getColumnNames(key)); } } else { strs.add(field.getName()); } } return strs; } public static String[] getColumnNames(Field field) { Column c = field.getAnnotation(Column.class); if(c != null && c.value().length > 0) return c.value(); // default mapping: field names if(isModel(field.getType())) { ClassInfo ci = getClassInfo(field.getType()); List<String> keys = new ArrayList<String>(); // if no @column is provided // if the model has one single key, we use the local field name // if the model has several keys, we concatenate the fieldName+"_"+keyName if(ci.keys.size()==1){ return new String[] { field.getName() }; } for (Field key : ci.keys) { // uses the prefix fieldName_ to prevent problem with models having the same field names keys.addAll(Arrays.asList(getColumnNamesWithPrefix(key, field.getName()+"_"))); } return keys.toArray(new String[keys.size()]); } return new String[]{ field.getName() }; } public static String getSingleColumnName(Field field) { Column c = field.getAnnotation(Column.class); if(c != null && c.value().length > 0) return c.value()[0]; // default mapping: field names if(isModel(field.getType())) { ClassInfo ci = getClassInfo(field.getType()); String keys = ""; // if no @column is provided // if the model has one single key, we use the local field name // if the model has several keys, we concatenate the fieldName+"_"+keyName if(ci.keys.size()==1){ return field.getName(); } // multi keys returns field_key1:field_key2 int i=0; int sz = ci.keys.size(); for (Field key : ci.keys) { // uses the prefix fieldName_ to prevent problem with models having the same field names keys += field.getName()+"_"+ getSingleColumnName(key); if(i < sz){ keys += ":"; } i++; } return keys; } return field.getName(); } public static String getSimplestColumnName(Field field) { Column c = field.getAnnotation(Column.class); if(c != null && c.value().length > 0) return c.value()[0]; return field.getName(); } public static String[] getColumnNamesWithPrefix(Field field, String prefix) { Column c = field.getAnnotation(Column.class); if(c != null && c.value().length > 0) { String[] cols = c.value(); for(int i=0;i<cols.length;i++){ cols[i]=prefix+cols[i]; } return cols; } // default mapping: field names if(isModel(field.getType())) { ClassInfo ci = getClassInfo(field.getType()); List<String> keys = new ArrayList<String>(); // if no @column is provided // if the model has one single key, we use the local field name // if the model has several keys, we concatenate the fieldName+"_"+keyName if(ci.keys.size()==1){ return new String[] { prefix+field.getName() }; } for (Field key : ci.keys) { // concatenates prefix with new prefix keys.addAll(Arrays.asList(getColumnNamesWithPrefix(key, prefix+field.getName()+"_"))); } return keys.toArray(new String[keys.size()]); } return new String[]{ prefix + field.getName() }; } public static String[] getColumnNames(Field field, String tableName) { Column c = field.getAnnotation(Column.class); if(c != null && c.value().length > 0) { if(tableName!=null && !("".equals(tableName))){ String[] cols = c.value(); for(int i=0;i<cols.length;i++){ cols[i]=tableName+"."+cols[i]; } return cols; } else return c.value(); } // default mapping: field names if(isModel(field.getType())) { ClassInfo ci = getClassInfo(field.getType()); List<String> keys = new ArrayList<String>(); // if no @column is provided // if the model has one single key, we use the local field name // if the model has several keys, we concatenate the fieldName+"_"+keyName if(ci.keys.size()==1){ if(tableName!=null && !("".equals(tableName))){ return new String[] { tableName+"."+field.getName() }; }else { return new String[] { field.getName() }; } } for (Field key : ci.keys) { if(tableName!=null && !("".equals(tableName))){ keys.addAll(Arrays.asList(getColumnNamesWithPrefix(key, tableName+"."+field.getName()+"_"))); }else { keys.addAll(Arrays.asList(getColumnNamesWithPrefix(key, field.getName()+"_"))); } } return keys.toArray(new String[keys.size()]); } if(tableName!=null && !("".equals(tableName))) return new String[]{ tableName+"."+field.getName() }; else return new String[]{ field.getName() }; } public static boolean isModel(Class<?> type) { // this way is much better in Java syntax if(Model.class.isAssignableFrom(type)){ /*if(type.getSuperclass() == Model.class)*/ return true; } // TODO: this needs to be tested // TODO what if type is NULL???? if(type.getName().startsWith("java.")) { return false; } /*if(type == Json.class)*/ if(Json.class.isAssignableFrom(type)) { return false; } ClassInfo info = ClassInfo.findClassInfo(type); if(info != null){ return !info.keys.isEmpty(); } else { return scanIdInHierarchy(type); } } public static boolean isId(Field field) { return field.isAnnotationPresent(Id.class); } public static boolean isEmbedded(Field field) { return field.isAnnotationPresent(Embedded.class); } public static boolean isAggregated(Field field) { return field.isAnnotationPresent(Aggregated.class); } public static boolean isAggregator(Field field) { return field.isAnnotationPresent(Aggregator.class); } public static boolean isJoined(Field field) { return field.isAnnotationPresent(Join.class); } public static boolean isMany(Field field) { return Many.class.isAssignableFrom(field.getType()); } public static boolean isOne(Field field) { return One.class.isAssignableFrom(field.getType()); } public static boolean isOwned(Field field) { return field.isAnnotationPresent(Owned.class); } public static boolean isOwner(Field field) { return field.isAnnotationPresent(Owner.class); } public static boolean isGenerated(Field field) { Id id = field.getAnnotation(Id.class); if(id != null) { Class<?> type = field.getType(); // ONLY long ID can be auto_incremented if(id.value() == Generator.AUTO_INCREMENT && ( Long.TYPE == type || Long.class.isAssignableFrom(type))) { return true; } if(id.value() == Generator.UUID && (String.class.isAssignableFrom(type) || UUID.class.isAssignableFrom(type))) { return true; } } return false; } public static boolean isAutoIncrement(Field field) { Id id = field.getAnnotation(Id.class); if(id != null) { Class<?> type = field.getType(); // ONLY long ID can be auto_incremented if(id.value() == Generator.AUTO_INCREMENT && ( Long.TYPE == type || Long.class.isAssignableFrom(type))) { return true; } } return false; } public static boolean isEmbeddedNative(Field field) { Embedded embed = field.getAnnotation(Embedded.class); if(embed != null && embed.mode() == Embedded.Mode.NATIVE){ return true; } return false; } /** * Useful for those PersistenceManagers that only support one @Id * @param clazz * @return */ public static Field getIdField(Class<?> clazz) { List<Field> keys = ClassInfo.getClassInfo(clazz).keys; if(keys.isEmpty()) throw new SienaException("No valid @Id defined in class "+clazz.getName()); if(keys.size() > 1) throw new SienaException("Multiple @Id defined in class "+clazz.getName()); return keys.get(0); } /** * Useful for those PersistenceManagers that only support one @Id * @param clazz * @return */ public Field getIdField() { if(keys.isEmpty()) throw new SienaException("No valid @Id defined in class "+tableName); if(keys.size() > 1) throw new SienaException("Multiple @Id defined in class "+tableName); return keys.get(0); } public static ClassInfo getClassInfo(Class<?> clazz) { ClassInfo ci = infoClasses.get(clazz); if(ci == null) { ci = new ClassInfo(clazz); infoClasses.put(clazz, ci); } return ci; } public static ClassInfo findClassInfo(Class<?> clazz) { return infoClasses.get(clazz); } public List<Method> getLifeCycleMethod(LifeCyclePhase lcp){ return lifecycleMethods.get(lcp); } public Field getFirstFieldFromType(Class<?> fieldType){ for(Field f: updateFields){ if(f.getType().isAssignableFrom(fieldType)){ return f; } } return null; } }