package my.home.dsl.validation;
import java.lang.reflect.Field;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import my.home.dsl.deepClone.BaseType;
import my.home.dsl.deepClone.ClassCloner;
import my.home.dsl.deepClone.ComplexField;
import my.home.dsl.deepClone.ContainerType;
import my.home.dsl.deepClone.DeepClonePackage;
import my.home.dsl.deepClone.FieldClonerType;
import my.home.dsl.utils.DeepCloneUtils;
import my.home.dsl.utils.ReflectionUtils;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.xtext.common.types.JvmTypeReference;
import org.eclipse.xtext.validation.Check;
import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
/**
* DeepClone DSL main validator. It is called after parsing, when model AST is
* created.
* <p>
* It also sets inferred Java types for all Field like objects. Attribute
* {@link BaseType#getJavaType()} is a transient attribute in the model.
*
* @author espinosa
*/
public class DeepCloneJavaValidator extends AbstractDeepCloneJavaValidator {
@Inject ReflectionUtils utils;
@Inject DeepCloneUtils dcUtils;
/**
* Prevent cloning of certain bean properties, like 'class', that is {@link Object#getClass()}.
* @param fieldCloner
*/
@Check
public void checkFieldForbiddenNames(FieldClonerType fieldCloner) {
if (fieldCloner.getFieldName().equals("class")) {
error("Field cannot be named 'class', introspection ", DeepClonePackage.Literals.FIELD_CLONER_TYPE__FIELD_NAME);
}
}
public static final String FIELD_DEFINITION_NOT_UNIQUE_IN_CONTAINER_ERROR = "Missing field error";
/**
* Every member field must be defined once in its parent container
* @param fieldCloner
*/
@Check
public void checkFieldUniquity(FieldClonerType fieldCloner) {
ContainerType parentContainer = ((ContainerType)fieldCloner.eContainer());
List<FieldClonerType> declaredfields = parentContainer.getFields();
int uniquityCounter = 0;
for (FieldClonerType otherField : declaredfields) {
if (otherField.getFieldName().equals(fieldCloner.getFieldName())) uniquityCounter++;
if (uniquityCounter>1) {
error("Field " + fieldCloner.getFieldName() + " is declared more then once in container " + dcUtils.getNodeQualifiedName(parentContainer),
fieldCloner,
DeepClonePackage.Literals.FIELD_CLONER_TYPE__FIELD_NAME,
FIELD_DEFINITION_NOT_UNIQUE_IN_CONTAINER_ERROR, fieldCloner.getFieldName());
break;
}
}
}
/**
* Check class existence for root Cloner. Root cloner has to have its type
* specified in the model. All other field types will be inferred upon this
* type.<br>
* Set {@link BaseType#setJavaType(JvmTypeReference)}. Upon this value all
* type inference is based recursively for all member fields and
* sub-structures.
*
* @param rootCloner
*/
@Check
public void checkRootClonerClass(ClassCloner rootCloner) {
// copying from one JvmTypeReference to another JvmTypeReference field is so straightforward
// without defensive copy of the, the first field would get nullified, upon setting the second! (a xtext bug?)
rootCloner.setJavaType(utils.createDefensiveCopyOfJvmTypeReference(rootCloner.getClassToClone()));
// there is no real checking of type/class existence since we use XBase's JvmTypeReference and
// it takes care of type existence already
}
/**
* Infer field type based on parent container type and current field name.
* Check its existence. Set {@link BaseType#setJavaType(JvmTypeReference)}.
* Upon this value all type inference is based recursively for all member
* fields and sub-structures.
*
* @param fieldCloner
* current field reference
*/
@Check
public void checkFieldClass(FieldClonerType fieldCloner) {
JvmTypeReference parentType = ((ContainerType)fieldCloner.eContainer()).getJavaType();
if (parentType!=null) {
JvmTypeReference clazz = utils.getFieldType(
utils.getTypeOrCollectionTypeParameter(
parentType).getType(),
fieldCloner.getFieldName());
if (clazz != null) {
fieldCloner.setJavaType(utils.createDefensiveCopyOfJvmTypeReference(clazz));
}
}
// there is no real checking of type/class existence since we use XBase's JvmTypeReference and
// it takes care of type existence already
}
public static final String MISSING_FIELD_ERROR = "Missing field error";
public static final String SURPLUS_FIELD_ERROR = "Surplus field error";
public static final String MISSING_FIELDS_ERROR = "Missing fields error";
public static final String SURPLUS_FIELDS_ERROR = "Surplus fields error";
/**
* Check all member fields (bean properties), defined in the model against
* introspection members. They must match perfectly. Every introspection
* field must have equivalent in the cloner definition. Field is either
* explicitly included or explicitly excluded. This include complex fields
* and cloner references too.
*
* @param container
* a container type - ClassCloner or ComplexField - reference
*/
@Check
public void checkPresenceOfAllFields(ContainerType container) {
List<FieldClonerType> declaredfields = container.getFields();
if (container.getJavaType()==null) return;
List<String> reflectionFields = utils.getFieldNamesForClassTreatCollections(container.getJavaType());
Set<String> reflectionFieldNames = new HashSet<String>(reflectionFields);
Set<String> declaredfieldsNames = new HashSet<String>(Lists.transform(declaredfields, fieldClonerNameFunc));
Set<String> missingDeclarations = Sets.difference(reflectionFieldNames, declaredfieldsNames);
Set<String> surplusDeclarations = Sets.difference(declaredfieldsNames, reflectionFieldNames);
int i = 0;
for (String missingFieldName : missingDeclarations) {
error("The cloner for type " + getContainerName(container) + " must exclude or include field " + missingFieldName, // message
container, // the error is associated with parent container
findMarkingFeatureForMissingField(container), // structural feature; indication what to underline as an error, for a cloner it is its name
MISSING_FIELD_ERROR, // error code
getIssueData(missingDeclarations, i) // issue data
);
i++;
}
i = 0;
for (String surplusFieldName : surplusDeclarations) {
error("The cloner for type " + getContainerName(container) + " references unknown field " + surplusFieldName, // message
Iterables.find(container.getFields(), ReflectionUtils.byName(surplusFieldName)), // the error is associated with filed itself, we have to locate it first
DeepClonePackage.Literals.FIELD_CLONER_TYPE__FIELD_NAME, // structural feature; indication what to underline as an error, for a surplus filed it is its name
SURPLUS_FIELD_ERROR, // error code
getIssueData(surplusDeclarations, i) // issueData
);
i++;
}
}
protected String[] getIssueData(Set<String> fields, int fieldCounter) {
if (fieldCounter == 0) {
return fields.toArray(new String[]{});
} else {
return null;
}
}
protected String getContainerName(ContainerType container) {
if (container instanceof ClassCloner) {
return ((ClassCloner)container).getClassToClone().getQualifiedName();
} else if (container instanceof ComplexField) {
return ((ComplexField)container).getJavaType().getQualifiedName();
} else {
return "???";
}
}
/**
* Example results:
* CLASS_CLONER__CLASS_TO_CLONE - color only class in ClassCloner
* null - color whole element (lots of red lines)
* @param container
* @return
*/
protected EStructuralFeature findMarkingFeatureForMissingField(ContainerType container) {
if (container instanceof ClassCloner) {
if (((ClassCloner)container).getName() != null) {
return DeepClonePackage.Literals.CLASS_CLONER__NAME;
} else {
return DeepClonePackage.Literals.CLASS_CLONER__CLASS_TO_CLONE;
}
} else if (container instanceof ComplexField) {
return DeepClonePackage.Literals.FIELD_CLONER_TYPE__FIELD_NAME;
} else {
return null;
}
}
/**
* Predicate for Google Collections filtering
*/
@SuppressWarnings("unused")
private Function<Field, String> reflFieldNameFunc = new Function<Field, String>() {
@Override public String apply(Field f) {
return f.getName();
}
};
/**
* Predicate for Google Collections filtering
*/
private Function<FieldClonerType, String> fieldClonerNameFunc = new Function<FieldClonerType, String>() {
@Override public String apply(FieldClonerType f) {
return f.getFieldName();
}
};
}