/*
* Copyright 2012 Adaptrex, LLC
*
* 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.adaptrex.core.ext.data;
import com.adaptrex.core.persistence.AdaptrexPersistence;
import com.adaptrex.core.persistence.AdaptrexSession;
import com.adaptrex.core.utilities.StringUtilities;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.text.SimpleDateFormat;
import java.util.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/*
* ModelInstance is basically a wrapper around a map that contains the data
* for a specific instance of a database record. This can be deeply nested
* and include ManyToOne and OneToMany relationships.
*
* This class is responsible for building a map in an Ext friendly format and
* relies heavily on the ORM implementation to retrieve the data used to build
* the map
*/
public class ModelInstance {
private static Logger log = LoggerFactory.getLogger(ModelInstance.class);
/*
* Config contains all of the information necessary to determine the data
* we want to retrieving from the store. It also contains some formatting
* information about the response we want to send back to Ext.
*/
private DataConfig config;
private AdaptrexSession session;
/*
* This holds the response
*/
private Map<String, Object> data = new HashMap<String, Object>();
/*
* Ext wants dates formatted like this. Make sure we convert all dates
* to that format. In the future we may want this to be configurable
* but I'm unsure what situation would require it.
*/
private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
private static SimpleDateFormat timeFormat = new SimpleDateFormat("kk:mm:ss");
/*
* Create the model instance
*/
public ModelInstance(AdaptrexSession session, DataConfig config, Object entity) {
this.config = config;
this.session = session;
data = buildEntityMap(session, entity, "", null);
}
/*
* This generates a map for each entity in the tree. It recurses on itself
* to generate the map for all associated objects.
*
* In order to keep track of where we are in the tree, we use entityNamePath.
* This is used to determine if certain fields should be included/excluded
* or if we've got a join to a deeper foreign entity.
*
* parentEntity is used to make sure we don't attempt to associate the current
* entity with it's own parent creating an infinte recursion
*/
private Map<String, Object> buildEntityMap(AdaptrexSession session, Object entity, String parentEntityPath, Object parentEntity) {
AdaptrexPersistence persistence = session.getPersistence();
Map<String, Object> entityMap = new HashMap<String, Object>();
if (entity == null) {
return entityMap;
}
Class<?> entityClazz = entity.getClass();
boolean isRoot = parentEntityPath.equals("");
String entityPath = parentEntityPath + entity.getClass().getSimpleName();
/*
* In order to determine whether or not a field on this entity should be
* included, we need to determine whether it has been explicitly
* included or excluded, or if it's a foreign association, we need to know
* if it's been explicitly included.
*
* The config object contains settings for the entire tree. We want to make
* sure we're only looking at the settings for this node (entity)
*/
List<String> entityIncludes = new ArrayList<String>();
List<String> entityExcludes = new ArrayList<String>();
List<String> entityJoins = new ArrayList<String>();
if (isRoot) {
/*
* Loop each include/exclude/join config and determine if they
* are set for the root entity.
*/
for (String incl : this.config.getIncludes()) {
if (!incl.contains(".")) {
entityIncludes.add(incl);
}
}
for (String excl : this.config.getExcludes()) {
if (!excl.contains(".")) {
entityExcludes.add(excl);
}
}
for (String join : this.config.getAssociationEntityNames()) {
if (!join.contains(".")) {
entityJoins.add(join);
}
}
} else {
/*
* Loop each include/exclude/join config and determine if they are set for
* the current entity at this position in the tree. If the full path to a
* specific field on this entity is set, we want to include it in the entity
* specific config. We only need the field portion when testing against
* the current entity so that gets split out before adding to the list
*/
for (String incl : this.config.getIncludes()) {
if (incl.contains(entityPath + ".")) {
entityIncludes.add(incl.split("\\.")[1]);
}
}
for (String excl : this.config.getExcludes()) {
if (excl.contains(entityPath + ".")) {
entityExcludes.add(excl.split("\\.")[1]);
}
}
for (String join : this.config.getAssociationEntityNames()) {
if (join.contains(entityPath + ".")) {
entityJoins.add(join = join.split("\\.")[1]);
}
}
}
/*
* Loop through fields on our entity and determine if they should be added
*/
for (Field field : entityClazz.getDeclaredFields()) {
/*
* Static fields don't get returned (reveng may add static fields
* to the class)
*/
if (Modifier.isStatic(field.getModifiers())) {
continue;
}
/*
* Get the field name
*/
String fieldName = field.getName();
/*
* Confirm we should be including this field for this entity. ID fields
* get added regardless of whether they are explicitly included.
*
*/
if (!persistence.isIdField(entityClazz, fieldName)) {
if (this.config.getIncludes().size() > 0) {
if (entityIncludes.isEmpty()
|| (!entityIncludes.contains("*") && !entityIncludes.contains(fieldName))) {
continue;
}
}
if (this.config.getIncludes().size() > 0) {
if (entityExcludes.contains("*") || entityExcludes.contains(fieldName)) {
continue;
}
}
}
/*
* Get the field value and make sure it's not this entities parent (prevent infinite recursion)
*/
Object fieldValue = persistence.getFieldValue(entity, fieldName);
if (!isRoot && fieldValue != null && fieldValue.equals(parentEntity)) {
continue;
}
/*
* Process one to many
*/
if (persistence.isOneToMany(entityClazz, fieldName)) {
if (!entityJoins.contains(StringUtilities.capitalize(fieldName))) {
continue;
}
try {
List<Map<String, Object>> associatedData = new ArrayList<Map<String, Object>>();
@SuppressWarnings("unchecked")
List<Object> assocObjList = new ArrayList<Object>((Collection<? extends Object>) fieldValue);
for (Object assocObj : assocObjList) {
/*
* Don't loop back and add our parent entity (prevent infinite recursion)
*/
if (parentEntity != null && assocObj.equals(parentEntity)) {
continue;
}
associatedData.add(buildEntityMap(session, assocObj, entityPath, entity));
}
entityMap.put(fieldName, associatedData);
} catch (Exception e) {
log.warn("Error", e);
}
continue;
}
/*
* Process many to one
*/
if (persistence.isManyToOne(entityClazz, fieldName)) {
if (!entityJoins.contains(StringUtilities.capitalize(fieldName))) {
continue;
}
if (fieldValue == null) {
entityMap.put(fieldName, null);
continue;
}
try {
entityMap.put(fieldName, buildEntityMap(session, fieldValue, entityPath, entity));
/*
* We also want the ID of our nested store... Ext should set this
* automatically I think. TODO: Need to customize "id" field
*/
//Method idGetter = fieldValue.getClass().getMethod("getId");
//entityMap.put(fieldName + "Id", idGetter.invoke(fieldValue));
} catch (Exception e) {
log.warn("Error", e);
}
continue;
}
/*
* Process standard fields
*/
try {
if (fieldValue == null) {
entityMap.put(fieldName, null);
continue;
}
String fieldType = persistence.getFieldType(entityClazz, fieldName);
if (fieldType.equals("date")) {
fieldValue = dateFormat.format(fieldValue);
} else if (fieldType.equals("time")) {
fieldValue = timeFormat.format(fieldValue);
}
entityMap.put(fieldName, fieldValue);
} catch (Exception e) {
log.warn("Error", e);
continue;
}
}
return entityMap;
}
public Map<String, Object> getData() {
return data;
}
}