/*
* Copyright (c) 2008 Borland Software Corporation
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Artem Tikhomirov (Borland) - initial API and implementation
*/
package org.eclipse.gmf.internal.validate;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.emf.common.util.BasicDiagnostic;
import org.eclipse.emf.common.util.Diagnostic;
import org.eclipse.emf.common.util.DiagnosticChain;
import org.eclipse.emf.ecore.EAnnotation;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EClassifier;
import org.eclipse.emf.ecore.EModelElement;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EOperation;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.gmf.internal.validate.expressions.EnvironmentProvider;
import org.eclipse.gmf.internal.validate.expressions.ExpressionProviderRegistry;
import org.eclipse.gmf.internal.validate.expressions.IModelExpression;
import org.eclipse.gmf.internal.validate.expressions.IModelExpressionProvider;
import org.eclipse.gmf.internal.validate.expressions.IParseEnvironment;
import org.eclipse.gmf.internal.validate.expressions.NoProviderExpression;
import org.eclipse.gmf.validate.ValidationOptions;
import org.eclipse.osgi.util.NLS;
/**
* @author artem
*/
class ExpressionCache {
private final HashMap<EClass, List<ConstraintAdapter>> myClass2Constraints;
public ExpressionCache() {
myClass2Constraints = new HashMap<EClass, List<ConstraintAdapter>>();
}
public Validator get(EClass context, DiagnosticChain diag, ExternModelImport modelImports) {
LinkedList<ConstraintAdapter> result = new LinkedList<ConstraintAdapter>();
List<ConstraintAdapter> l = myClass2Constraints.get(context);
if (l == null) {
l = extract(context, diag, modelImports);
myClass2Constraints.put(context, l);
}
result.addAll(l);
for (EClass s : context.getEAllSuperTypes()) {
l = myClass2Constraints.get(s);
if (l == null) {
l = extract(s, diag, modelImports);
myClass2Constraints.put(s, l);
}
result.addAll(l);
}
if (result.isEmpty()) {
return null;
}
return new Validator(result);
}
public static Validator get(EAnnotation ann, DiagnosticChain diag, ExternModelImport modelImports) {
if (!Annotations.CONSTRAINTS_URI.equals(ann.getSource())) {
return null;
}
EModelElement e = ann.getEModelElement();
EClass contextClass = e instanceof EClass ? (EClass) e : (e instanceof EStructuralFeature ? ((EStructuralFeature) e).getEContainingClass(): null) ;
if (contextClass != null) {
List<ConstraintAdapter> r = extract(Collections.singletonList(ann), contextClass, diag, modelImports);
if (r.isEmpty()) {
return null;
}
return new Validator(r);
} else {
diag.add(new BasicDiagnostic(Diagnostic.WARNING, AbstractValidator.DIAGNOSTIC_SOURCE, StatusCodes.INVALID_CONSTRAINT_CONTEXT, MessageFormat.format(
Messages.validation_ConstraintInInvalidContext, new Object[] { LabelProvider.INSTANCE.getObjectLabel(e) }), new Object[] { ann }));
}
return null;
}
private static List<ConstraintAdapter> extract(EClass eClass, DiagnosticChain diag, ExternModelImport modelImports) {
LinkedList<EAnnotation> constraintAnnotations = new LinkedList<EAnnotation>();
for (EAnnotation a : eClass.getEAnnotations()) {
if (Annotations.CONSTRAINTS_URI.equals(a.getSource())) {
constraintAnnotations.add(a);
}
}
for (EOperation nextOperation : eClass.getEOperations()) {
for (EAnnotation a : nextOperation.getEAnnotations()) {
if (Annotations.CONSTRAINTS_URI.equals(a.getSource())) {
constraintAnnotations.add(a);
}
}
}
for (EStructuralFeature nextFeature : eClass.getEStructuralFeatures()) {
for (EAnnotation a : nextFeature.getEAnnotations()) {
if (Annotations.CONSTRAINTS_URI.equals(a.getSource())) {
constraintAnnotations.add(a);
}
}
}
return extract(constraintAnnotations, eClass, diag, modelImports);
}
private static List<ConstraintAdapter> extract(List<EAnnotation> constraintAnnotations, EClass eClass, DiagnosticChain diag, ExternModelImport modelImports) {
if (constraintAnnotations.isEmpty()) {
return Collections.<ConstraintAdapter>emptyList();
}
LinkedList<ConstraintAdapter> result = new LinkedList<ConstraintAdapter>();
// actually, next code may result in few Constraints per same constrain definition (EAnnotation)
// with few languages/expressions defined. Perhaps, java classes should rather mimic
// definition structure (one constraint, few expressions) then create constraint-per-expression?
for (EAnnotation ann : constraintAnnotations) {
final int severity = getDiagnosticSeverity(ann, diag);
final String description = getDescriptionDetail(ann);
for (Map.Entry<String, String> nextDetail : ann.getDetails()) {
String key = String.valueOf(nextDetail.getKey());
if (ExpressionProviderRegistry.getInstance().getLanguages().contains(key)) {
String body = readBodyDetail(nextDetail, diag);
if (body != null) {
IModelExpression expression = getExpression(key, body, eClass, modelImports);
ConstraintAdapter constraint = new ConstraintAdapter(expression, severity, description);
result.add(constraint);
}
}
}
}
return result;
}
private static IModelExpression getExpression(String language, String body, EClassifier context, ExternModelImport modelImports) {
IModelExpressionProvider provider = ExpressionProviderRegistry.getInstance().getProvider(language);
if (provider == null) {
return new NoProviderExpression(language, body, context);
}
IParseEnvironment env = null;
if(modelImports != null && modelImports.getPackageRegistry() != null) {
env = EnvironmentProvider.createParseEnv();
env.setImportRegistry(modelImports.getPackageRegistry());
}
return provider.createExpression(body, context, env);
}
private static int getDiagnosticSeverity(EAnnotation constraintAnnotation, DiagnosticChain diagnostics) {
int severity = Diagnostic.ERROR; // default and also fall-back value
Object val = constraintAnnotation.getDetails().get(Annotations.SEVERITY);
String strVal = (val instanceof String) ? ((String) val).trim() : null;
if (Annotations.SEVERITY_INFO.equals(strVal)) {
severity = Diagnostic.INFO;
} else if (Annotations.SEVERITY_WARN.equals(strVal)) {
severity = Diagnostic.WARNING;
} else if (Annotations.SEVERITY_ERROR.equals(strVal)) {
severity = Diagnostic.ERROR;
} else if (strVal != null) {
diagnostics.add(new BasicDiagnostic(Diagnostic.ERROR, AbstractValidator.DIAGNOSTIC_SOURCE, StatusCodes.INVALID_CONSTRAINT_SEVERITY, NLS.bind(Messages.invalidConstraintSeverity, new Object[] { strVal,
Annotations.SEVERITY_ERROR, Annotations.SEVERITY_WARN, Annotations.SEVERITY_INFO }), new Object[] { val }));
}
return severity;
}
private static String getDescriptionDetail(EAnnotation annotation) {
Object val = annotation.getDetails().get(Annotations.DESCRIPTION);
return val != null ? String.valueOf(val) : null;
}
private static String readBodyDetail(Map.Entry<String, String> bodyEntry, DiagnosticChain diagnostics) {
String body = bodyEntry.getValue();
if (body != null && body.trim().length() > 0) {
return body;
}
diagnostics.add(new BasicDiagnostic(Diagnostic.WARNING, AbstractValidator.DIAGNOSTIC_SOURCE, StatusCodes.EMPTY_CONSTRAINT_BODY, Messages.validation_EmptyExpressionBody, new Object[] { bodyEntry }));
return null;
}
static class Validator {
private final List<ConstraintAdapter> myConstraints;
public Validator(List<ConstraintAdapter> constraints) {
assert constraints != null;
myConstraints = constraints;
}
public boolean validate(EObject modelElement, DiagnosticChain diagnostics, ValidationOptions opts) {
boolean isValid = true;
for(ConstraintAdapter x : myConstraints) {
isValid &= handleConstraintDefinition(x, diagnostics);
isValid &= handleConstrainedElement(x, modelElement, diagnostics, opts);
}
return isValid;
}
public boolean checkConstraints(DiagnosticChain diagnostics) {
boolean isValid = true;
for (ConstraintAdapter constraint : myConstraints) {
isValid &= handleConstraintDefinition(constraint, diagnostics);
}
return isValid;
}
private boolean handleConstraintDefinition(ConstraintAdapter constraintProxy, DiagnosticChain diagnostics) {
IStatus constraintStatus = constraintProxy.getStatus();
if (Trace.shouldTrace(DebugOptions.META_DEFINITIONS)) {
String msgPtn = "[metamodel-constraint] context={0} body={1}"; //$NON-NLS-1$
Trace.trace(MessageFormat.format(msgPtn, new Object[] { LabelProvider.INSTANCE.getObjectLabel(constraintProxy.getContext()), constraintProxy.getBody() }));
}
if (!constraintStatus.isOK()) {
String message = MessageFormat.format(Messages.invalidExpressionBody, new Object[] { constraintProxy.getBody(), constraintStatus.getMessage() });
diagnostics.add(new BasicDiagnostic(Diagnostic.ERROR, AbstractValidator.DIAGNOSTIC_SOURCE, constraintStatus.getCode(), message, new Object[] { constraintProxy.getContext() }));
return false;
}
return true;
}
private static boolean handleConstrainedElement(ConstraintAdapter constraint, EObject constrainedElement, DiagnosticChain diagnostics, ValidationOptions opts) {
if (!constraint.isSatisfied(constrainedElement)) {
String message = null;
if (constraint.getDescription() == null) {
message = MessageFormat.format(Messages.validation_ConstraintViolation, new Object[] { constraint.getBody(), LabelProvider.INSTANCE.getObjectLabel(constrainedElement) });
} else {
// TODO - user constraint ID as a key, support localication for messages
message = constraint.getDescription();
}
diagnostics.add(new BasicDiagnostic(constraint.getSeverity(), AbstractValidator.DIAGNOSTIC_SOURCE, StatusCodes.CONSTRAINT_VIOLATION, message, new Object[] { constrainedElement }));
return false;
} else {
if (opts.isReportSuccess()) {
diagnostics.add(new BasicDiagnostic(Diagnostic.OK, AbstractValidator.DIAGNOSTIC_SOURCE, StatusCodes.CONSTRAINT_SATISFIED, MessageFormat.format(Messages.validation_ConstraintSatisfied, new Object[] {
constraint.getBody(), LabelProvider.INSTANCE.getObjectLabel(constrainedElement) }), new Object[] { constrainedElement }));
}
}
return true;
}
}
}