/************************************************************************
* Copyright (c) 2014-2016 IoT-Solutions e.U.
*
* 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 iot.jcypher.domain.mapping;
import java.util.Collection;
import java.util.Map;
import iot.jcypher.domain.mapping.CompoundObjectType.CType;
import iot.jcypher.domain.mapping.surrogate.InnerClassSurrogate;
import iot.jcypher.graph.GrNode;
import iot.jcypher.graph.GrProperty;
public class FieldMapping {
public static final String ClassFieldSeparator = "|";
private static transient final String InnerRefField = "this$";
private IField field;
private String fieldName;
protected String propertyName;
private String classFieldName;
public FieldMapping(IField field) {
this(field, field.getName());
}
public FieldMapping(IField field, String propertyName) {
super();
this.field = field;
this.field.setAccessible(true);
this.propertyName = propertyName;
}
public boolean isInnerClassRefField() {
return this.getFieldName().startsWith(InnerRefField);
}
public void mapPropertyFromField(Object domainObject, GrNode rNode) {
intMapPropertyFromField(domainObject, rNode);
}
protected Object intMapPropertyFromField(Object domainObject, GrNode rNode) {
Object ret = null;
try {
prepare(domainObject);
if (getObjectNeedingRelation(domainObject) == null) { // also checks against DomainInfo
// we can map to a property
Object value = this.field.get(domainObject);
ret = value;
Object mappedValue = MappingUtil.convertToProperty(value);
GrProperty prop = rNode.getProperty(this.propertyName);
if (mappedValue != null) {
boolean propModified = false;
if (prop != null) {
Object propValue = MappingUtil.convertFromProperty(prop.getValue(), mappedValue.getClass(),
getComponentType(rNode), getConcreteFieldType());
if (!propValue.equals(mappedValue)) {
prop.setValue(mappedValue);
propModified = true;
}
} else {
rNode.addProperty(this.propertyName, mappedValue);
propModified = true;
}
if (propModified)
storeSimpleListComponentType(value, rNode);
} else {
if (prop != null) // remove the property
prop.setValue(null);
}
} else {
boolean clearAdditional = false;
// a previously empty collection or array might have been mapped to a property
// we need to remove the property
if (Collection.class.isAssignableFrom(getFieldType()) || getFieldType().isArray()) {
GrProperty prop = rNode.getProperty(this.propertyName);
if (prop != null)
prop.setValue(null);
clearAdditional = true;
}
// a previously empty map might have been mapped to a property
// we need to remove the property
if (Map.class.isAssignableFrom(getFieldType())) {
GrProperty prop = rNode.getProperty(this.propertyName);
if (prop != null)
prop.setValue(null);
clearAdditional = true;
}
if (clearAdditional) // e.g. type property
clearAdditionalProperties(rNode);
}
} catch (Throwable e) {
throw new RuntimeException(e);
}
return ret;
}
protected void clearAdditionalProperties(GrNode rNode) {
// do nothing in this implementation
// overwritten by subclasses
}
/**
* @param domainObject
* @param rNode
* @return true if the property exists in the node
*/
public boolean mapPropertyToField(Object domainObject, GrNode rNode) {
boolean hasProperty = false;
if (domainObject instanceof InnerClassSurrogate) {
hasProperty = ((InnerClassSurrogate)domainObject).addPropertyChild(this, domainObject, rNode);
} else {
try {
prepare(domainObject);
Object value = this.field.get(domainObject);
GrProperty prop = rNode.getProperty(this.propertyName);
if (prop != null) {
hasProperty = true;
Object propValue = prop.getValue();
if (propValue != null) { // allow null values in properties
Class<?> typ = getFieldTypeInt(rNode);
propValue = MappingUtil.convertFromProperty(propValue, typ,
getComponentType(rNode), getConcreteFieldType());
if (!propValue.equals(value)) {
this.field.set(domainObject, convertFieldValue(propValue, domainObject));
}
}
}
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
return hasProperty;
}
@SuppressWarnings("unchecked")
protected Object convertFieldValue(Object val, Object domainObject) {
Object ret = val;
if (domainObject instanceof iot.jcypher.domain.mapping.surrogate.Collection) {
iot.jcypher.domain.mapping.surrogate.Collection surrColl = (iot.jcypher.domain.mapping.surrogate.Collection)domainObject;
if (getFieldName().equals("collType") && surrColl.getContent() != null && val != null)
surrColl.setContent((Collection<Object>) MappingUtil.convertCollection(surrColl.getContent(), val.toString()));
else if (getFieldName().equals("c_content") && surrColl.getCollType() != null && val != null) {
ret = MappingUtil.convertCollection((Collection<?>)val, surrColl.getCollType());
}
}
return ret;
}
public void setFieldValue(Object domainObject, Object value) {
if (domainObject instanceof InnerClassSurrogate) {
((InnerClassSurrogate)domainObject).addChild(this, value);
} else if (value instanceof InnerClassSurrogate) {
((InnerClassSurrogate)value).addParent(this, domainObject);
} else {
try {
prepare(domainObject);
this.field.set(domainObject, value);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}
public Object getFieldValue(Object domainObject) {
try {
prepare(domainObject);
return this.field.get(domainObject);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
/**
*
* @return the value of the field, if this value cannot be mapped to a property,
* but must be mapped to a seperate node connected via a relation, else return null.
*/
@SuppressWarnings("rawtypes")
public Object getObjectNeedingRelation(Object domainObject) {
Object value = null;
try {
prepare(domainObject);
value = this.field.get(domainObject);
if (value != null && value.getClass().isAnonymousClass())
value = null; // TODO currently anonymous classes are not mapped
// because for reinstantiation we would need dynamic proxies
if (value != null) {
if (MappingUtil.isSimpleType(value.getClass())) { // value is of primitive or simple type
value = null;
} else {
Class<?> fieldType = this.field.getType();
// check for list (collection) or arrays containing primitive or simple types
Object elem = null;
boolean testCollOrArray = false;
if (Collection.class.isAssignableFrom(fieldType)) {
testCollOrArray = true;
Collection coll = (Collection) fieldType.cast(value);
if (coll.size() > 0)
elem = coll.iterator().next();
}
if (fieldType.isArray()) {
if (fieldType.getComponentType().isPrimitive())
value = null;
else {
testCollOrArray = true;
Object[] array = (Object[]) fieldType.cast(value);
if (array.length > 0)
elem = array[0];
}
}
if (testCollOrArray) {
if (elem != null) { // first element of collection or array
Class<?> type = elem.getClass();
// test the first element,
// assuming all elements are either complex or simple !!!
if (MappingUtil.isSimpleType(type)) { // elements are of primitive or simple type
// to return null
value = null;
}
} else { // empty lists or arrays are mapped to a property
value = null;
}
}
if (Map.class.isAssignableFrom(fieldType)) {
Map map = (Map)fieldType.cast(value);
if (map.isEmpty()) // empty maps are mapped to a property
value = null;
}
}
}
} catch (Throwable e) {
throw new RuntimeException(e);
}
return value;
}
/**
* @return true if the field type is Object.class or a map or list or array, because at runtime this
* can lead to a simple type (e.g. Integer), or to an empty or simple type array which can be mapped to a property,
* or it can lead to a complex type which requires a relation in the graph.
* It is therefore necessary to look at properties and relations.
*/
public boolean needsRelationOrProperty() {
return this.field.getType().equals(Object.class) ||
this.getFieldKind() == FieldKind.MAP ||
this.getFieldKind() == FieldKind.COLLECTION ||
this.getFieldKind() == FieldKind.ARRAY;
}
/**
*
* @return true, if this field cannot be mapped to a property,
* but must be mapped to a seperate node connected via a relation, else return false.
*/
public boolean needsRelation() {
boolean needRelation = !MappingUtil.mapsToProperty(this.field.getType());
if (needRelation) { // check DomainInfo
String classField = getClassFieldName();
CompoundObjectType cType = MappingUtil.internalDomainAccess.get()
.getConcreteFieldType(classField);
if (cType != null && cType.getCType() == CType.SIMPLE)
return false;
else {
// check for list (collection) or array containing primitive or simple types
// in DomainInfo
if (Collection.class.isAssignableFrom(this.field.getType()) || this.field.getType().isArray()) {
cType = MappingUtil.internalDomainAccess.get()
.getFieldComponentType(classField);
if (cType != null) {
needRelation = cType.getCType() != CType.SIMPLE;
} else
needRelation = true; // cannot determine if the component type is simple
// so return true and leave the decision for later,
// when a concrete component is available
}
}
}
return needRelation;
}
protected void storeSimpleListComponentType(Object value, GrNode rNode) {
// do nothing in this implementation
// overwritten by subclasses
}
/**
* only called when to check for a concrete simple component type
* @return
*/
protected Class<?> getComponentType(GrNode rNode) {
// do nothing in this implementation except for primitive arrays
// overwritten by subclasses
Class<?> compType;
if (this.getFieldType().isArray() && (compType = this.getFieldType().getComponentType()).isPrimitive()) {
return compType;
}
return null;
}
private Class<?> getConcreteFieldType() {
if (getFieldKind() == FieldKind.MAP) {
String classField = getClassFieldName();
CompoundObjectType cType = MappingUtil.internalDomainAccess.get()
.getConcreteFieldType(classField);
if (cType != null)
return cType.getType();
}
return null;
}
public String getPropertyOrRelationName() {
return this.propertyName;
}
public IField getField() {
return this.field;
}
public Class<?> getFieldType () {
return this.field.getType();
}
protected Class<?> getFieldTypeInt (GrNode rNode) {
return this.field.getType();
}
public String getFieldName() {
if (this.fieldName == null)
this.fieldName = this.field.getName();
return this.fieldName;
}
private void prepare(Object domainObject) throws NoSuchFieldException, SecurityException {
if (this.fieldName == null)
this.fieldName = this.field.getName();
}
public String getClassFieldName() {
if (this.classFieldName == null) {
this.classFieldName = createClassFieldName();
}
return this.classFieldName;
}
private String createClassFieldName() {
StringBuilder sb = new StringBuilder();
sb.append(this.field.getDeclaringClass().getName());
sb.append(ClassFieldSeparator);
sb.append(this.getFieldName());
return sb.toString();
}
public FieldKind getFieldKind() {
Class<?> typ = this.field.getType();
return getFieldKind(typ);
}
public static FieldKind getFieldKind(Class<?> typ) {
return Collection.class.isAssignableFrom(typ) ? FieldKind.COLLECTION :
Map.class.isAssignableFrom(typ) ? FieldKind.MAP :
typ.isArray() && !typ.getComponentType().isPrimitive() ? FieldKind.ARRAY : FieldKind.SINGLE;
// TODO what about Date values -> they are seen as simple types
}
protected String getDOClassFieldName() {
if (this.classFieldName == null) {
this.classFieldName = createClassFieldName();
}
return this.classFieldName;
}
protected String getDOPropertyOrRelationName() {
return this.propertyName;
}
@Override
public int hashCode() {
return field.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (!(obj instanceof FieldMapping))
return false;
FieldMapping other = (FieldMapping) obj;
if (field == null) {
if (other.getField() != null)
return false;
} else if (!field.equals(other.getField()))
return false;
return true;
}
protected Class<?> getTypeFromProperty(GrNode rNode, String propertyName) {
GrProperty typeProp = rNode.getProperty(propertyName);
Class<?> clazz = null;
if (typeProp != null) {
try {
clazz = MappingUtil.internalDomainAccess.get().getClassForName(typeProp.getValue().toString());
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
return clazz;
}
/***********************************/
public static enum FieldKind {
SINGLE, COLLECTION, ARRAY, MAP
}
}