package my.home.dsl.utils;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import my.home.dsl.deepClone.BaseType;
import my.home.dsl.deepClone.Body;
import my.home.dsl.deepClone.ClassCloner;
import my.home.dsl.deepClone.ContainerType;
import my.home.dsl.deepClone.DeepCloneFactory;
import my.home.dsl.deepClone.FieldClonerType;
import my.home.dsl.deepClone.SimpleField;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.xtext.common.types.JvmField;
import org.eclipse.xtext.common.types.JvmTypeReference;
import org.eclipse.xtext.common.types.impl.JvmUnknownTypeReferenceImplCustom;
import org.eclipse.xtext.common.types.util.TypeReferences;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
@SuppressWarnings("restriction")
public class DeepCloneUtils {
@Inject
ReflectionUtils reflectionUtils;
@Inject
TypeReferences typeReferences;
/**
* Assure that given element has correctly assigned corresponding Java type.
* <p>
* If it does not trigger type inferring process which has to be done
* for whole model top down from root, see {@link #assureAllJavaTypesAreInferred(EObject)}
* for more details.
*
* @param element
*/
public void assureJavaTypesIsInferred(BaseType element) {
if (element.getJavaType() == null) {
assureAllJavaTypesAreInferred(element);
}
}
/**
* Assure that all Java types for whole model AST are inferred. All cloner
* fields - simple, complex, nested - have their Java type assigned to them.
* <p>
* In DeepClone Java types are stored directly in the model itself as
* transient values; some operations on model may however cause reset of all
* transient values. In some use cases it must be explicitly ensured that
* all Java types are assigned.
*
*
* @param anyElement
* root any node from the model AST; if the give element is not
* root, root is found based on given element bottom up
*/
public void assureAllJavaTypesAreInferred(EObject anyElement) {
inferJavaTypes(findModelRoot(anyElement));
}
/**
* Infer Java types for given element and all its nested elements.
* Recursive method typically called for root element to
* ensure all Java types are assigned for the whole semantic model.
*
* @param parent element
*/
public void inferJavaTypes(Body modelRoot) {
inferJavaTypesForElement(modelRoot);
}
// TODO: refactor!
protected void inferJavaTypesForElement(EObject modelElement) {
for (EObject child : modelElement.eContents()) {
if (child instanceof ClassCloner) {
ClassCloner rootCloner = (ClassCloner) child;
if (rootCloner.getClassToClone() != null && !rootCloner.getClassToClone().getType().eIsProxy()) {
rootCloner.setJavaType(reflectionUtils
.createDefensiveCopyOfJvmTypeReference(rootCloner
.getClassToClone()));
} else {
rootCloner.setJavaType(reflectionUtils
.createDefensiveCopyOfJvmTypeReference(JVM_UNKNOWN_TYPE));
}
} else if (child instanceof FieldClonerType) {
FieldClonerType fieldCloner = (FieldClonerType) child;
JvmTypeReference parentType = ((ContainerType) fieldCloner
.eContainer()).getJavaType();
JvmField jvmField = reflectionUtils.getField(
parentType.getType(), fieldCloner.getFieldName());
if (jvmField != null) {
fieldCloner.setJavaType(reflectionUtils
.createDefensiveCopyOfJvmTypeReference(
reflectionUtils.getTypeOrCollectionTypeParameter(jvmField.getType())));
} else {
fieldCloner.setJavaType(reflectionUtils
.createDefensiveCopyOfJvmTypeReference(JVM_UNKNOWN_TYPE));
}
}
if (child.eContents() != null && child.eContents().size() > 0) {
inferJavaTypesForElement(child);
}
}
}
/**
* A dummy marker Jvm reference type to indicate that type cannot be inferred,
* that is field declared in model is not present in Java class (validation error in fact).
*/
public static final JvmTypeReference JVM_UNKNOWN_TYPE = new JvmUnknownTypeReferenceImplCustom();
/**
* Find model root from any model AST node
*
* @param element
* any node from model AST
* @return root node
*/
public Body findModelRoot(EObject element) {
EObject parent;
while ((parent = element.eContainer()) != null) {
element = parent;
}
if (element instanceof Body) {
return (Body)element;
} else {
throw new RuntimeException("Root element must of type " + Body.class);
}
}
/**
* Predicate for Google Collections filtering
*/
public Function<Field, String> reflFieldNameFunc = new Function<Field, String>() {
@Override
public String apply(Field f) {
return f.getName();
}
};
/**
* Predicate for Google Collections filtering
*/
public Function<FieldClonerType, String> fieldClonerNameFunc = new Function<FieldClonerType, String>() {
@Override
public String apply(FieldClonerType f) {
return f.getFieldName();
}
};
/**
* @return true if given field is a collection type, like List, Set, .. Used
* to check 1:N relations between beans.
*/
public boolean isCollection(FieldClonerType field) {
return typeReferences.isInstanceOf(field.getJavaType(),
Collection.class);
}
/**
* Get full name for given Model element. Intended for use in toString()
* style methods and error messages. Skip parts with no name or unrecognised
* name.
*/
public String getNodeQualifiedName(EObject element) {
List<String> result = new LinkedList<String>();
do {
String name = getSuitableNameForElement(element);
if (name != null) {
result.add(name);
}
} while ((element = element.eContainer()) != null);
return Joiner.on(".").join(Lists.reverse(result));
}
/**
* Get suitable name for given Model element. Intended for use in toString()
* style methods and error messages. Get the name for known types from the
* DSL only.
*/
public String getSuitableNameForElement(EObject element) {
String result = null;
if (element instanceof FieldClonerType) {
result = ((FieldClonerType) element).getFieldName();
} else if (element instanceof ClassCloner) {
ClassCloner cc = (ClassCloner) element;
if (cc.getName() != null) {
result = cc.getName();
} else if (cc.getClassToClone() != null
&& cc.getClassToClone().getType() != null) {
result = cc.getClassToClone().getType().getSimpleName();
}
}
return result;
}
/**
* Remove given fields from a given parent container, Container type field
* or root cloner. Model AST manipulation method.
*
* @param parentContainer
* @param fieldsToBeRemoved
*/
public void removeFields(ContainerType parentContainer,
List<String> fieldsToBeRemoved) {
Preconditions.checkNotNull(parentContainer);
List<FieldClonerType> fields = parentContainer.getFields();
for (String fieldToBeRemoved : fieldsToBeRemoved) {
Iterator<FieldClonerType> fieldIterator = fields.iterator();
while (fieldIterator.hasNext()) {
if (fieldIterator.next().getFieldName()
.equals(fieldToBeRemoved)) {
fieldIterator.remove();
}
}
}
}
/**
* Add new fields to a parent container, Container type field or root
* cloner. Model AST manipulation method.
*
* @param parentContainer
* @param fieldsToBeAdded
* list of field names
*/
public void addFields(ContainerType parentContainer,
List<String> fieldsToBeAdded) {
DeepCloneFactory deepCloneFactory = DeepCloneFactory.eINSTANCE;
for (String newfieldName : fieldsToBeAdded) {
SimpleField field = deepCloneFactory.createSimpleField();
field.setFieldName(newfieldName);
parentContainer.getFields().add(field);
}
}
/**
* Convenient casting extension method, handy for "one-liners"
*
* @param field
* @return
*/
public ContainerType asContainer(BaseType field) {
return (ContainerType) field;
}
/**
* Find all missing fields (undeclared in model) for a container element based on
* referenced Java bean class members.
* <p>
* In another words, find the difference between class fields (fields declared in Java class, all cloneable Java Bean members)
* and model fields (fields declared for given container element so far).
* <p>
* Example:
* <p>
* If model model declares:
* <pre>
* a.b.c.Person PersonCloner {
* firstName
* surname
* }
* </pre>
* And Java class declares:
* <pre>
* class a.b.c.Person {
* String firstName;
* String surname
* String middleName
* Date dateOfBirth
* }
* </pre>
* Return would be set:
* <pre>
* ["middlename", "dateOfBirth"]
* </pre>
*
* @param container model container type element, only containers can have fields (sub-elements)
* @return field names or empty set
*/
public Set<String> findMissingFieldsInContainerElement(
ContainerType container) {
assureJavaTypesIsInferred(container);
List<FieldClonerType> declaredfields = container.getFields();
List<String> classFields = reflectionUtils.getFieldNamesForClass(container.getJavaType());
Set<String> classFieldsSet = new HashSet<String>(classFields);
Set<String> modelFieldsSet = new HashSet<String>(Lists.transform(declaredfields, fieldClonerNameFunc));
Set<String> missingFieldsSet = Sets.difference(classFieldsSet, modelFieldsSet);
return missingFieldsSet;
}
}