Commit b815efb6 authored by Daniel Bluhm's avatar Daniel Bluhm

Merge branch 'e2e-test' into annotation-extraction

Signed-off-by: Daniel Bluhm's avatarDaniel Bluhm <bluhmdj@ornl.gov>
parents 5ef31b33 b04d2bc5
...@@ -21,5 +21,6 @@ ...@@ -21,5 +21,6 @@
<module>../org.eclipse.ice.dev</module> <module>../org.eclipse.ice.dev</module>
<module>../org.eclipse.ice.archetypes</module> <module>../org.eclipse.ice.archetypes</module>
<module>../org.eclipse.ice.commands</module> <module>../org.eclipse.ice.commands</module>
<module>../org.eclipse.ice.renderer</module>
</modules> </modules>
</project> </project>
...@@ -30,6 +30,10 @@ import javax.lang.model.element.ElementKind; ...@@ -30,6 +30,10 @@ import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement; import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements; import javax.lang.model.util.Elements;
import javax.tools.Diagnostic; import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.JavaFileManager.Location;
import javax.tools.JavaFileObject;
import javax.tools.StandardLocation;
import org.apache.velocity.app.Velocity; import org.apache.velocity.app.Velocity;
import org.eclipse.ice.dev.annotations.DataElement; import org.eclipse.ice.dev.annotations.DataElement;
...@@ -119,7 +123,6 @@ public class DataElementProcessor extends AbstractProcessor { ...@@ -119,7 +123,6 @@ public class DataElementProcessor extends AbstractProcessor {
// Iterate over all elements with DataElement Annotation // Iterate over all elements with DataElement Annotation
for (final Element elem : roundEnv.getElementsAnnotatedWith(DataElement.class)) { for (final Element elem : roundEnv.getElementsAnnotatedWith(DataElement.class)) {
try { try {
if (!valid(elem)) if (!valid(elem))
throw new InvalidDataElementSpec("DataElementSpec must be class, found " + elem.toString()); throw new InvalidDataElementSpec("DataElementSpec must be class, found " + elem.toString());
...@@ -127,7 +130,6 @@ public class DataElementProcessor extends AbstractProcessor { ...@@ -127,7 +130,6 @@ public class DataElementProcessor extends AbstractProcessor {
.className(extractName(elem)).build(); .className(extractName(elem)).build();
extractor.generateAndWrite(request); extractor.generateAndWrite(request);
} catch (final IOException | InvalidDataElementSpec e) { } catch (final IOException | InvalidDataElementSpec e) {
messager.printMessage(Diagnostic.Kind.ERROR, stackTraceToString(e)); messager.printMessage(Diagnostic.Kind.ERROR, stackTraceToString(e));
return false; return false;
...@@ -154,4 +156,33 @@ public class DataElementProcessor extends AbstractProcessor { ...@@ -154,4 +156,33 @@ public class DataElementProcessor extends AbstractProcessor {
private boolean valid(Element element) { private boolean valid(Element element) {
return element.getKind() == ElementKind.CLASS; return element.getKind() == ElementKind.CLASS;
} }
/**
* Write the TypeScript of DataElement annotated class to file.
* @param element
* @param fields
* @throws IOException
*/
private void writeTypeScript(
DataElementSpec element,
Fields fields
) throws IOException {
final FileObject generatedFile = processingEnv.getFiler()
.createResource(
StandardLocation.SOURCE_OUTPUT,
"",
"frontend/" + element.getName() + ".ts"
);
try (Writer writer = generatedFile.openWriter()) {
Fields trimmed = fields.getNonDefaultFields();
TypeScriptWriter.builder()
.name(element.getName())
.fields(trimmed)
.types(trimmed.getTypes())
.build()
.write(writer);
} catch (UnsupportedOperationException e) {
messager.printMessage(Diagnostic.Kind.NOTE, stackTraceToString(e));
}
}
} }
package org.eclipse.ice.dev.annotations.processors;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import org.eclipse.ice.dev.annotations.DataField;
/**
* An AnnotatedElement subclass representing a DataField.
* @author Daniel Bluhm
*/
public class DataFieldSpec extends AnnotatedElement {
/**
* Set of Annotation names that extractAnnotations should filter out.
*/
private static final Set<String> ANNOTATION_CLASS_NAMES = Set.of(
DataField.class,
DataField.Default.class
).stream()
.map(cls -> cls.getCanonicalName())
.collect(Collectors.toSet());
/**
* Used to get DataField Annotation values.
*/
private DataField fieldInfo;
/**
* Instantiate a DataFieldSpec.
* @param element annotated with {@code @DataField}
* @param elementUtils Elements helper class from processing environment
*/
public DataFieldSpec(Element element, Elements elementUtils) {
super(element, elementUtils);
this.fieldInfo = this.element.getAnnotation(DataField.class);
}
/**
* Determine if the passed field is a DataField.
* @param element to check
* @return whether element is a DataField
*/
public static boolean isDataField(Element element) {
return element.getAnnotation(DataField.class) != null;
}
/**
* Return the set of access modifiers on this Field.
* @return extract field modifiers
* @see Modifier
*/
private Set<Modifier> extractModifiers() {
return this.element.getModifiers();
}
/**
* Return the set of annotations on this DataField, excepting the DataField
* Annotation itself.
* @return extracted annotations, excluding DataField related annotations
*/
private List<String> extractAnnotations() {
return this.element.getAnnotationMirrors().stream()
.filter(mirror -> !ANNOTATION_CLASS_NAMES.contains(
mirror.getAnnotationType().toString()
))
.map(mirror -> mirror.toString())
.collect(Collectors.toList());
}
/**
* Return the class of this Field.
* @return extracted field type
*/
private TypeMirror extractFieldType() {
return this.element.asType();
}
/**
* Return the name of this Field.
* @return extracted field name
*/
private String extractFieldName() {
return this.element.getSimpleName().toString();
}
/**
* Return the DocString of this Field.
* @return extracted doc comment
*/
private String extractDocString() {
return this.elementUtils.getDocComment(this.element);
}
/**
* Extract the defaultValue of this Field. Checks for {@link DataField.Default}
* and if not present checks for a constant expression if the field is
* {@code final}.
* @return extracted default value
*/
private String extractDefaultValue() {
String retval = null;
DataField.Default defaults = this.element.getAnnotation(DataField.Default.class);
if (defaults != null) {
if (defaults.isString()) {
retval = this.elementUtils.getConstantExpression(defaults.value());
} else {
retval = defaults.value();
}
} else if (this.element.getModifiers().contains(Modifier.FINAL)) {
retval = this.elementUtils.getConstantExpression(
((VariableElement) this.element).getConstantValue()
);
}
return retval;
}
/**
* Return this DataFieldSpec as a Field.
* @return field
*/
public Field toField() {
return Field.builder()
.name(extractFieldName())
.type(extractFieldType())
.defaultValue(extractDefaultValue())
.docString(extractDocString())
.annotations(extractAnnotations())
.modifiersToString(extractModifiers())
.getter(fieldInfo.getter())
.setter(fieldInfo.setter())
.match(fieldInfo.match())
.unique(fieldInfo.unique())
.searchable(fieldInfo.searchable())
.nullable(fieldInfo.nullable())
.build();
}
}
...@@ -91,6 +91,18 @@ public class Fields implements Iterable<Field> { ...@@ -91,6 +91,18 @@ public class Fields implements Iterable<Field> {
.filter(field -> !field.isDefaultField()) .filter(field -> !field.isDefaultField())
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
/**
* Return Fields that are not marked as default.
* @return Fields new instance with default fields filtered out.
*/
public Fields getNonDefaultFields() {
return new Fields(
fields.stream()
.filter(field -> !field.isDefaultField())
.collect(Collectors.toList())
);
}
/** /**
* Return Types instance for this set of Fields. * Return Types instance for this set of Fields.
......
/*******************************************************************************
* Copyright (c) 2020- UT-Battelle, LLC.
* 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:
* Daniel Bluhm - Initial implementation
*******************************************************************************/
package org.eclipse.ice.dev.annotations.processors;
import java.util.Map;
import lombok.Builder;
import lombok.NonNull;
/**
* Writer for TypeScript representation of DataElement.
* @author Daniel Bluhm
*/
public class TypeScriptWriter extends VelocitySourceWriter {
/**
* Template used for this writer.
*/
private static final String TEMPLATE = "templates/TypeScript.vm";
/**
* Context key for name.
*/
private static final String NAME = "name";
/**
* Context key for fields.
*/
private static final String FIELDS = "fields";
/**
* Context key for types.
*/
private static final String TYPES = "types";
/**
* Context key for primitiveMap.
*/
private static final String PRIMITIVE_MAP = "primitiveMap";
/**
* Map of Java primitive + String type strings to TypeScript type strings.
*/
private static Map<String, String> primitiveMap = Map.ofEntries(
Map.entry("java.lang.String", "string"),
Map.entry("boolean", "boolean"),
Map.entry("float", "number"),
Map.entry("long", "number"),
Map.entry("int", "number"),
Map.entry("double", "number"),
Map.entry("java.util.Date", "Date")
);
/**
* Create Writer.
* @param name of TypeScript class generated.
* @param fields present on data element.
* @param types of fields.
* @throws UnsupportedOperationException When any field is not supported.
*/
@Builder
public TypeScriptWriter(
String name, @NonNull Fields fields, @NonNull Types types
) throws UnsupportedOperationException {
super();
for (Field field : fields) {
if (!primitiveMap.containsKey(field.getType())) {
throw new UnsupportedOperationException(String.format(
"Field %s: type %s is unsupported",
field.getName(), field.getType()
));
}
}
this.template = TEMPLATE;
this.context.put(NAME, name);
this.context.put(FIELDS, fields);
this.context.put(TYPES, types);
this.context.put(PRIMITIVE_MAP, primitiveMap);
}
}
\ No newline at end of file
...@@ -13,12 +13,16 @@ import com.fasterxml.jackson.annotation.JsonCreator; ...@@ -13,12 +13,16 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.NonNull; import lombok.NonNull;
import lombok.Setter; import lombok.Setter;
...@@ -28,7 +32,9 @@ import lombok.Setter; ...@@ -28,7 +32,9 @@ import lombok.Setter;
* This is an implementation of $interface that satisfies the dependencies of * This is an implementation of $interface that satisfies the dependencies of
* the @DataElement Annotation and was auto-generated by the ICE Framework. * the @DataElement Annotation and was auto-generated by the ICE Framework.
*/ */
@Builder
@Data @Data
@AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
@JsonAutoDetect( @JsonAutoDetect(
fieldVisibility = Visibility.ANY, fieldVisibility = Visibility.ANY,
...@@ -37,12 +43,22 @@ import lombok.Setter; ...@@ -37,12 +43,22 @@ import lombok.Setter;
setterVisibility = Visibility.NONE setterVisibility = Visibility.NONE
) )
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
@JsonDeserialize(builder = ${class}.${class}Builder.class)
public class ${class} implements ${interface}, Serializable { public class ${class} implements ${interface}, Serializable {
/** /**
* Logging tool * Logging tool
*/ */
private static final Logger logger = LoggerFactory.getLogger(${class}.class); private static final Logger logger = LoggerFactory.getLogger(${class}.class);
/**
* Deserialization helpers.
*/
@JsonPOJOBuilder(withPrefix = "")
public static class ${class}Builder implements ${class}BuilderMeta {
}
private interface ${class}BuilderMeta {
}
#foreach($field in $fields) #foreach($field in $fields)
#fielddoc #fielddoc
...@@ -53,7 +69,7 @@ public class ${class} implements ${interface}, Serializable { ...@@ -53,7 +69,7 @@ public class ${class} implements ${interface}, Serializable {
#if(!${field.Setter} || ${field.VarNameDifferent}) #if(!${field.Setter} || ${field.VarNameDifferent})
@Setter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
#end #end
#fielddecl #fielddecl(true)
#end #end
#foreach($field in ${fields.Constants}) #foreach($field in ${fields.Constants})
...@@ -82,25 +98,6 @@ public class ${class} implements ${interface}, Serializable { ...@@ -82,25 +98,6 @@ public class ${class} implements ${interface}, Serializable {
#end #end
#end #end
/**
* All args constructor for $class.
*
* Used in JSON Deserialization.
#foreach($field in $fields)
* @param ${field.VarName} {@code #fieldtype} for field ${field.VarName}
#end
*/
@JsonCreator
public $class(
#foreach($field in $fields)
@JsonProperty("${field.VarName}") #fieldparametertype ${field.VarName}#if($foreach.hasNext),#end
#end
) {
#foreach($field in $fields)
this.${field.VarName} = ${field.VarName};
#end
}
/** /**
* Copy constructor for $class. * Copy constructor for $class.
* @param other Instance of $class to copy * @param other Instance of $class to copy
......
/**
* $name DataElement.
*/
import { DataElement } from "DataElement";
export class $name extends DataElement {
#foreach($field in $fields)
${field.VarName}?: $primitiveMap["${field.Type}"];
#end
constructor() {
super();
}
}
\ No newline at end of file
...@@ -50,9 +50,9 @@ ...@@ -50,9 +50,9 @@
#macro(fieldparametertype)#nonnull("", " ")#fieldtype#end #macro(fieldparametertype)#nonnull("", " ")#fieldtype#end
## Get field declaration ## Get field declaration
#macro(fielddecl) #macro(fielddecl $mutable)
#@settab(1) #@settab(1)
#join(" ", ${field.Annotations})#join(" ", ${field.Modifiers})#fieldtype() ${field.VarName}#if(${field.DefaultValue}) = #evaluate(${field.DefaultValue})#end; #if(${field.DefaultValue} && $mutable) @Builder.Default #end#join(" ", ${field.Annotations})#join(" ", ${field.Modifiers})#fieldtype() ${field.VarName}#if(${field.DefaultValue}) = #evaluate(${field.DefaultValue})#end;
#end #end
#end #end
......
@Builder
@Data @Data
@AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
@JsonAutoDetect( @JsonAutoDetect(
fieldVisibility = Visibility.ANY, fieldVisibility = Visibility.ANY,
...@@ -7,6 +9,7 @@ ...@@ -7,6 +9,7 @@
setterVisibility = Visibility.NONE setterVisibility = Visibility.NONE
) )
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
@JsonDeserialize(builder = TestImplementation.TestImplementationBuilder.class)
public class TestImplementation implements Test, Serializable { public class TestImplementation implements Test, Serializable {
public int shouldBePublic; public int shouldBePublic;
protected int shouldBeProtected; protected int shouldBeProtected;
......
@Builder
@Data @Data
@AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
@JsonAutoDetect( @JsonAutoDetect(
fieldVisibility = Visibility.ANY, fieldVisibility = Visibility.ANY,
...@@ -7,6 +9,7 @@ ...@@ -7,6 +9,7 @@
setterVisibility = Visibility.NONE setterVisibility = Visibility.NONE
) )
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
@JsonDeserialize(builder = TestImplementation.TestImplementationBuilder.class)
public class TestImplementation implements Test, Serializable { public class TestImplementation implements Test, Serializable {
public int testInt = 42; @Builder.Default public int testInt = 42;
} }
\ No newline at end of file