/*******************************************************************************
* Copyright (c) 2017 itemis AG and others.
*
* 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:
* Alexander Nyßen (itemis AG) - initial API & implementation
*
*******************************************************************************/
package org.eclipse.gef.dot.internal.generator;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.eclipse.xtend.lib.macro.AbstractFieldProcessor;
import org.eclipse.xtend.lib.macro.TransformationContext;
import org.eclipse.xtend.lib.macro.declaration.AnnotationReference;
import org.eclipse.xtend.lib.macro.declaration.MutableFieldDeclaration;
import org.eclipse.xtend.lib.macro.declaration.MutableMethodDeclaration;
import org.eclipse.xtend.lib.macro.declaration.TypeReference;
/**
*
* An {@link AbstractFieldProcessor} for {@link DotAttribute} annotations.
*
* @author anyssen
*
*/
public class DotAttributeProcessor extends AbstractFieldProcessor {
private static Pattern NAMING_PATTERN = Pattern
.compile("[_A-Z]*[A-Z]+__(G?)(S?)(C?)(N?)(E?)");
private static Pattern CAMEL_CASE_REPLACEMENT_PATTERN = Pattern
.compile("(_?[a-z]+)_([a-z]+)");
/**
* Indication of the context in which an attribute is used.
*/
// TODO: Use G, N, E, S, C and replace parsing with Enum.valueOf()
private static enum Context {
GRAPH, NODE, EDGE, SUBGRAPH, CLUSTER
}
@Override
public void doTransform(MutableFieldDeclaration field,
TransformationContext context) {
// retrieve name (but do not validate it yet)
String attributeName = attributeName(field);
// XXX: Retrieve annotation values and cache them, as the annotation
// will be removed
// and the procedures are executed lazily
String[] attributeRawTypes = (String[]) annotationValue(field, context,
"rawType");
TypeReference[] attributeParsedTypes = (TypeReference[]) annotationValue(
field, context, "parsedType");
// field comment
field.setDocComment("The '" + attributeName
+ "' attribute, which is used by: "
+ usedBy(field).stream()
.map((f) -> "Cluster".contentEquals(paramTypeName(f))
? paramTypeName(f)
: "{@link " + paramTypeName(f) + "}")
.collect(Collectors.joining(", "))
+ ".");
// XXX: Naming conventions is checked by usedBy extension
List<Context> contexts = uniqueGraphTypes(usedBy(field));
for (int i = 0; i < contexts.size(); i++) {
Context c = contexts.get(i);
// we may specify different values for each context (the order has
// to match)
String attributeRawType = attributeRawTypes.length > 1
? attributeRawTypes[i] : attributeRawTypes[0];
TypeReference attributeParsedType = attributeParsedTypes.length > 1
? attributeParsedTypes[i] : attributeParsedTypes[0];
// raw getter
field.getDeclaringType().addMethod(rawGetterName(field),
(MutableMethodDeclaration method) -> {
field.markAsRead();
StringBuilder docComment = new StringBuilder();
docComment
.append("Returns the (raw) value of the {@link #"
+ field.getSimpleName()
+ "} attribute of the given {@link "
+ paramTypeName(c) + "}.\n");
docComment.append(" @param " + paramName(c) + "\n");
docComment.append(" The {@link "
+ paramTypeName(c)
+ "} for which to return the value of the\n");
docComment.append(" {@link #"
+ field.getSimpleName() + "} attribute.\n");
docComment
.append(" @return The (raw) value of the {@link #"
+ field.getSimpleName()
+ "} attribute of the given\n");
docComment.append(" {@link "
+ paramTypeName(c) + "}.\n");
method.setDocComment(docComment.toString());
method.setStatic(true);
method.addParameter(paramName(c),
paramType(c, context));
method.setReturnType(context.newTypeReference(
"org.eclipse.gef.dot.internal.language.terminals.ID"));
StringBuilder body = new StringBuilder();
body.append("return (ID) " + paramName(c)
+ ".attributesProperty().get("
+ field.getSimpleName() + ");");
method.setBody((ctx) -> body.toString());
context.setPrimarySourceElement(method, field);
});
// raw setter
field.getDeclaringType().addMethod(rawSetterName(field),
(MutableMethodDeclaration method) -> {
StringBuilder docComment = new StringBuilder();
docComment.append("Sets the (raw) value of the {@link #"
+ field.getSimpleName()
+ "} attribute of the given {@link "
+ paramTypeName(c) + "}\n");
docComment.append("to the given <i>" + attributeName
+ "</i> value.\n");
docComment.append(" @param " + paramName(c) + "\n");
docComment.append(" The {@link "
+ paramTypeName(c)
+ "} for which to change the value of the\n");
docComment.append(" {@link #"
+ field.getSimpleName() + "} attribute.\n");
docComment.append(" @param " + attributeName + "\n");
docComment
.append(" The new (raw) value of the {@link #"
+ field.getSimpleName()
+ "} attribute.\n");
docComment.append(
" @throws IllegalArgumentException\n");
docComment.append(" when the given <i>"
+ attributeName
+ "</i> value is not supported.\n");
method.setDocComment(docComment.toString());
method.setStatic(true);
method.addParameter(paramName(c),
paramType(c, context));
method.addParameter(attributeName,
context.newTypeReference(
"org.eclipse.gef.dot.internal.language.terminals.ID"));
StringBuilder body = new StringBuilder();
body.append("checkAttributeRawValue(Context."
+ c.name().toUpperCase() + ", "
+ field.getSimpleName() + ", " + attributeName
+ ");");
body.append(paramName(c) + ".attributesProperty().put("
+ field.getSimpleName() + ", " + attributeName
+ ");");
method.setBody((ctx) -> body.toString());
context.setPrimarySourceElement(method, field);
});
field.getDeclaringType().addMethod(getterName(field),
(MutableMethodDeclaration method) -> {
field.markAsRead();
StringBuilder docComment = new StringBuilder();
docComment.append("Returns the value of the {@link #"
+ field.getSimpleName()
+ "} attribute of the given {@link "
+ paramTypeName(c) + "}.\n");
docComment.append(" @param " + paramName(c) + "\n");
docComment.append(" The {@link "
+ paramTypeName(c)
+ "} for which to return the value of the {@link #"
+ field.getSimpleName() + "} attribute.\n");
docComment
.append(" @return The value of the {@link #"
+ field.getSimpleName()
+ "} attribute of the given {@link "
+ paramTypeName(c) + "}.\n");
method.setDocComment(docComment.toString());
method.setStatic(true);
method.addParameter(paramName(c),
paramType(c, context));
method.setReturnType(
context.newTypeReference(String.class));
StringBuilder body = new StringBuilder();
body.append("ID " + attributeName + "Raw = "
+ rawGetterName(field) + "(" + paramName(c)
+ ");\n");
body.append("return " + attributeName + "Raw != null ? "
+ attributeName + "Raw.toValue() : null;");
method.setBody((ctx) -> body.toString());
context.setPrimarySourceElement(method, field);
context.setPrimarySourceElement(method, field);
});
field.getDeclaringType().addMethod(setterName(field),
(MutableMethodDeclaration method) -> {
StringBuilder docComment = new StringBuilder();
docComment.append("Sets the value of the {@link #"
+ field.getSimpleName()
+ "} attribute of the given {@link "
+ paramTypeName(c) + "} to the given <i>"
+ attributeName + "</i> value.\n");
docComment.append(" @param " + paramName(c) + "\n");
docComment.append(" The {@link "
+ paramTypeName(c)
+ "} for which to change the value of the {@link #"
+ field.getSimpleName() + "} attribute.\n");
docComment.append(" @param " + attributeName + "\n");
docComment
.append(" The new value of the {@link #"
+ field.getSimpleName()
+ "} attribute.\n");
docComment.append(
" @throws IllegalArgumentException\n");
docComment.append(
" when the given <i>" + attributeName
+ "</i> value is not supported.\n");
method.setDocComment(docComment.toString());
method.setStatic(true);
method.addParameter(paramName(c),
paramType(c, context));
method.addParameter(attributeName,
context.newTypeReference(String.class));
StringBuilder body = new StringBuilder();
body.append(rawSetterName(field) + "(" + paramName(c)
+ ", ID.fromValue(" + attributeName);
if (!attributeRawType.isEmpty()) {
body.append(
", org.eclipse.gef.dot.internal.language.terminals.ID.Type."
+ attributeRawType);
}
body.append("));");
method.setBody((ctx) -> body.toString());
context.setPrimarySourceElement(method, field);
});
// only generate parsed getters and setters if the parsed type
// does not equal String
if (!context.newTypeReference(String.class)
.equals(attributeParsedType)) {
// parsed getter
field.getDeclaringType().addMethod(parsedGetterName(field),
(MutableMethodDeclaration method) -> {
field.markAsRead();
StringBuilder docComment = new StringBuilder();
docComment
.append("Returns the (parsed) value of the {@link #"
+ field.getSimpleName()
+ "} attribute of the given {@link "
+ paramTypeName(c) + "}.\n");
docComment.append(
" @param " + paramName(c) + "\n");
docComment.append(" The {@link "
+ paramTypeName(c)
+ "} for which to return the value of the {@link #"
+ field.getSimpleName() + "} attribute.\n");
docComment
.append(" @return The (parsed) value of the {@link #"
+ field.getSimpleName()
+ "} attribute of the given {@link "
+ paramTypeName(c) + "}.\n");
method.setDocComment(docComment.toString());
method.setStatic(true);
method.addParameter(paramName(c),
paramType(c, context));
method.setReturnType(attributeParsedType);
StringBuilder body = new StringBuilder();
body.append(
"return " + parsed(
getterName(field) + "("
+ paramName(c) + ")",
attributeParsedType) + ";");
method.setBody((ctx) -> body.toString());
context.setPrimarySourceElement(method, field);
});
// parsed setter
field.getDeclaringType().addMethod(parsedSetterName(field),
(MutableMethodDeclaration method) -> {
field.markAsRead();
StringBuilder docComment = new StringBuilder();
docComment
.append("Sets the (parsed) value of the {@link #"
+ field.getSimpleName()
+ "} attribute of the given {@link "
+ paramTypeName(c)
+ "} to the given <i>"
+ attributeName + "</i> value.\n");
docComment.append(
" @param " + paramName(c) + "\n");
docComment.append(" The {@link "
+ paramTypeName(c)
+ "} for which to change the value of the {@link #"
+ field.getSimpleName() + "} attribute.\n");
docComment.append(
" @param " + attributeName + "\n");
docComment
.append(" The new (parsed) value of the {@link #"
+ field.getSimpleName()
+ "} attribute.\n");
docComment.append(
" @throws IllegalArgumentException\n");
docComment
.append(" when the given <i>"
+ attributeName
+ "</i> value is not supported.\n");
method.setDocComment(docComment.toString());
method.setStatic(true);
method.addParameter(paramName(c),
paramType(c, context));
method.addParameter(attributeName,
attributeParsedType);
StringBuilder body = new StringBuilder();
body.append(
setterName(field) + "(" + paramName(c)
+ ", "
+ serialized(attributeName,
attributeParsedType)
+ ");");
method.setBody((ctx) -> body.toString());
context.setPrimarySourceElement(method, field);
});
}
}
// XXX: Ensure the DotAttribute annotation is removed from the generated
// field,
// so there is no runtime dependency on it.
for (AnnotationReference reference : field.getAnnotations()) {
if (context.newTypeReference(DotAttribute.class)
.equals(context.newTypeReference(
reference.getAnnotationTypeDeclaration()))) {
field.removeAnnotation(reference);
}
}
}
private String serialized(String attributeValue,
TypeReference attributeParsedType) {
if (String.class.getName().equals(attributeParsedType.getName())) {
// no further serialization needed for String
return attributeValue;
}
return "serializeAttributeValue(" + serializer(attributeParsedType)
+ ", " + attributeValue + ")";
}
private String parsed(String attributeValue,
TypeReference attributeParsedType) {
if (String.class.getName().equals(attributeParsedType.getName())) {
// no further parsing needed or string
return attributeValue;
}
return "parseAttributeValue(" + parser(attributeParsedType) + ", "
+ attributeValue + ")";
}
// TODO: handle String and enum values distinctively
private String serializer(TypeReference attributeParsedType) {
return dotTypeName(attributeParsedType) + "_SERIALIZER";
}
// TODO: handle String and enum values distinctively.
private String parser(TypeReference attributeParsedType) {
return dotTypeName(attributeParsedType) + "_PARSER";
}
private String dotTypeName(TypeReference attributeParsedType) {
String dotTypeName = attributeParsedType.getSimpleName().toUpperCase();
String attributeTypeSimpleName = attributeParsedType.getType()
.getSimpleName();
if (Integer.class.getSimpleName().equals(attributeTypeSimpleName)) {
dotTypeName = "INT";
} else if (Boolean.class.getSimpleName()
.equals(attributeTypeSimpleName)) {
dotTypeName = "BOOL";
}
return dotTypeName;
}
private List<Context> uniqueGraphTypes(List<Context> contexts) {
List<Context> uniqueContexts = new ArrayList<>();
for (Context context : contexts) {
Context c = context;
if (c == Context.SUBGRAPH || c == Context.CLUSTER) {
c = Context.GRAPH;
}
if (!uniqueContexts.contains(c)) {
uniqueContexts.add(c);
}
}
return uniqueContexts;
}
private String attributeName(MutableFieldDeclaration field) {
String rawValue = field.getInitializer().toString()
.replaceAll("^\"|\"$", "");
Matcher matcher = CAMEL_CASE_REPLACEMENT_PATTERN.matcher(rawValue);
if (matcher.matches()) {
return matcher.group(1) + toFirstUpper(matcher.group(2));
}
return rawValue;
}
private String rawGetterName(MutableFieldDeclaration field) {
return getterName(field) + "Raw";
}
private String rawSetterName(MutableFieldDeclaration field) {
return setterName(field) + "Raw";
}
private String getterName(MutableFieldDeclaration field) {
return "get" + toFirstUpper(attributeName(field));
}
private String setterName(MutableFieldDeclaration field) {
return "set" + toFirstUpper(attributeName(field));
}
private String toFirstUpper(String s) {
if (s.length() > 1) {
return s.substring(0, 1).toUpperCase() + s.substring(1);
} else if (s.length() == 1) {
return s.toUpperCase();
} else {
return "";
}
}
private String parsedSetterName(MutableFieldDeclaration field) {
return setterName(field) + "Parsed";
}
private String parsedGetterName(MutableFieldDeclaration field) {
return getterName(field) + "Parsed";
}
private Object annotationValue(MutableFieldDeclaration field,
TransformationContext context, String property) {
for (AnnotationReference reference : field.getAnnotations()) {
if (DotAttribute.class.getName().equals(reference
.getAnnotationTypeDeclaration().getQualifiedName())) {
return reference.getValue(property);
}
}
throw new IllegalArgumentException("No DotAttribute annotation found.");
}
private List<Context> usedBy(MutableFieldDeclaration field) {
List<Context> applicableContexts = new ArrayList<>();
Matcher matcher = NAMING_PATTERN.matcher(field.getSimpleName());
if (!matcher.matches()) {
throw new IllegalArgumentException(
"Field name does not match naming pattern "
+ NAMING_PATTERN);
}
// determine which contexts apply
if (!matcher.group(1).isEmpty()) {
applicableContexts.add(Context.GRAPH);
}
if (!matcher.group(2).isEmpty()) {
applicableContexts.add(Context.SUBGRAPH);
}
if (!matcher.group(3).isEmpty()) {
applicableContexts.add(Context.CLUSTER);
}
if (!matcher.group(4).isEmpty()) {
applicableContexts.add(Context.NODE);
}
if (!matcher.group(5).isEmpty()) {
applicableContexts.add(Context.EDGE);
}
return applicableContexts;
}
private String paramName(Context context) {
return context.name().toLowerCase();
}
private String paramTypeName(Context context) {
return toFirstUpper(context.name().toLowerCase());
}
private TypeReference paramType(Context c, TransformationContext context) {
switch (c) {
case GRAPH:
return context.newTypeReference("org.eclipse.gef.graph.Graph");
case NODE:
return context.newTypeReference("org.eclipse.gef.graph.Node");
case EDGE:
return context.newTypeReference("org.eclipse.gef.graph.Edge");
default:
throw new IllegalArgumentException(
"Cluster and Subgraph not yet supported.");
}
}
}