Commit 2f40ad21 authored by Daniel Bluhm's avatar Daniel Bluhm
Browse files

Load Fields from JSON file with @DataFieldJson


Signed-off-by: Daniel Bluhm's avatarDaniel Bluhm <bluhmdj@ornl.gov>
parent 80910185
......@@ -74,7 +74,7 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.2</version>
<scope>provided</scope>
<scope>compile</scope>
</dependency>
</dependencies>
......
package org.eclipse.ice.dev.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface DataFieldJson {
String[] value();
}
......@@ -2,8 +2,10 @@ package org.eclipse.ice.dev.annotations.processors;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
......@@ -25,13 +27,16 @@ import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import javax.tools.StandardLocation;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.eclipse.ice.dev.annotations.DataElement;
import org.eclipse.ice.dev.annotations.DataField;
import org.eclipse.ice.dev.annotations.DataFieldJson;
import org.eclipse.ice.dev.annotations.DataFields;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.auto.service.AutoService;
/**
......@@ -128,40 +133,9 @@ public class DataElementProcessor extends AbstractProcessor {
final Fields fields = new Fields();
fields.addAll(DefaultFields.get());
final List<? extends AnnotationMirror> mirrors = elem.getAnnotationMirrors();
try {
// Iterate over the AnnotationValues of AnnotationMirrors of type DataFields.
// DataFields present when more than one DataField annotation is used.
for (
final AnnotationValue value : mirrors.stream()
.filter(
mirror -> mirror.getAnnotationType().toString().equals(
DataFields.class.getCanonicalName()
)
)
.map(mirror -> getAnnotationValuesForMirror(elementUtils, mirror))
.flatMap(List::stream) // Flatten List<List<AnnotationValue> to List<AnnotationValue>
.collect(Collectors.toList())
) {
// Traditional for-loop used to allow raising an exception with unwrap if the
// field visitor returns an error result
unwrap(value.accept(fieldsVisitor, fields));
}
// Iterate over the AnnotationValues of AnnotationMirrors of type DataField.
// Only present when only one DataField annotation is used.
for (
final AnnotationValue value : mirrors.stream()
.filter(
mirror -> mirror.getAnnotationType().toString().equals(
DataField.class.getCanonicalName()
)
)
.map(mirror -> getAnnotationValuesForMirror(elementUtils, mirror))
.flatMap(List::stream)
.collect(Collectors.toList())
) {
unwrap(value.accept(fieldVisitor, fields));
}
fields.addAll(collectFromDataFields(elem));
fields.addAll(collectFromDataFieldJson(elem));
this.writeClass(((TypeElement) elem).getQualifiedName().toString(), fields);
} catch (final IOException | UnexpectedValueError e) {
messager.printMessage(Diagnostic.Kind.ERROR, stackTraceToString(e));
......@@ -171,6 +145,96 @@ public class DataElementProcessor extends AbstractProcessor {
return false;
}
/**
* Collect Fields from DataFieldJson Annotations.
*
* The JSON input files are searched for in the "CLASS_OUTPUT" location,
* meaning the same folder to which compiled class files will be output.
* JSON files placed in src/main/resources are moved to this location before
* the annotation processing phase and are therefore available at this
* location at the time of annotation processing.
*
* @param element potentially annotated with DataFieldJson
* @return discovered fields
* @throws IOException
*/
private Fields collectFromDataFieldJson(Element element) throws IOException {
final List<? extends AnnotationMirror> mirrors = element.getAnnotationMirrors();
Fields fields = new Fields();
// Iterate through AnnotationValues of AnnotationMirrors for DataFieldJson
for (
final AnnotationValue value : mirrors.stream()
.filter(
mirror -> mirror.getAnnotationType().toString().equals(
DataFieldJson.class.getCanonicalName()
)
)
.map(mirror -> getAnnotationValuesForMirror(elementUtils, mirror))
.flatMap(List::stream) // Flatten List<List<AnnotationValue> to List<AnnotationValue>
.collect(Collectors.toList())
) {
// Flatten the AnnotationValue List into List of Strings in Annotation
List<String> sources = ((List<? extends AnnotationValue>) value.getValue()).stream()
.map(val -> (String) val.getValue())
.collect(Collectors.toList());
// Iterate through each JSON Data Field source and attempt to read
// fields from JSON file.
for (String source : sources) {
Reader reader = processingEnv.getFiler()
.getResource(StandardLocation.CLASS_OUTPUT, "", source)
.openReader(false);
ObjectMapper mapper = new ObjectMapper();
fields.addAll(Arrays.asList(mapper.readValue(reader, Field[].class)));
}
}
return fields;
}
/**
* Collect Fields from DataField and DataFields Annotations.
*
* @param element potentially annotated with one or more DataField Annotations.
* @return discovered fields
* @throws UnexpectedValueError
*/
private Fields collectFromDataFields(Element element) throws UnexpectedValueError {
final List<? extends AnnotationMirror> mirrors = element.getAnnotationMirrors();
Fields fields = new Fields();
// Iterate over the AnnotationValues of AnnotationMirrors of type DataFields.
// DataFields present when more than one DataField annotation is used.
for (
final AnnotationValue value : mirrors.stream()
.filter(
mirror -> mirror.getAnnotationType().toString().equals(
DataFields.class.getCanonicalName()
)
)
.map(mirror -> getAnnotationValuesForMirror(elementUtils, mirror))
.flatMap(List::stream) // Flatten List<List<AnnotationValue> to List<AnnotationValue>
.collect(Collectors.toList())
) {
// Traditional for-loop used to allow raising an exception with unwrap if the
// field visitor returns an error result
unwrap(value.accept(fieldsVisitor, fields));
}
// Iterate over the AnnotationValues of AnnotationMirrors of type DataField.
// Only present when only one DataField annotation is used.
for (
final AnnotationValue value : mirrors.stream()
.filter(
mirror -> mirror.getAnnotationType().toString().equals(
DataField.class.getCanonicalName()
)
)
.map(mirror -> getAnnotationValuesForMirror(elementUtils, mirror))
.flatMap(List::stream)
.collect(Collectors.toList())
) {
unwrap(value.accept(fieldVisitor, fields));
}
return fields;
}
/**
* Write the implementation of DataElement annotated class to file.
* @param interfaceName the annotated interface name, used to determine package and
......
package org.eclipse.ice.dev.annotations.processors;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import lombok.Builder;
import lombok.Data;
import lombok.NonNull;
......@@ -12,6 +16,7 @@ import lombok.NonNull;
*/
@Data
@Builder
@JsonDeserialize(builder = Field.FieldBuilder.class)
public class Field {
/**
* Name of the field.
......@@ -51,8 +56,13 @@ public class Field {
/**
* Builder class for Field. This class must be a static inner class of Field in
* order to take advantage of Lombok's @Builder annotation. The methods defined
* here replace the defaults generated by Lombok.
* here replace the defaults generated by Lombok and provide a multiple-dispatch
* based mechanism for customizing output based on the passed type.
*
* The methods prefixed with "json" are used for deserializing values from
* JSON using Jackson and should not be used except for that case.
*/
@JsonPOJOBuilder(withPrefix = "json")
public static class FieldBuilder {
/**
* Format long as String for use as default value initializer.
......@@ -116,6 +126,81 @@ public class Field {
this.type = type.getValue();
return this;
}
/**
* Name builder for use in Deserialization.
* @param name
* @return builder
*/
@JsonAlias({"fieldName"})
public FieldBuilder jsonName(String name) {
this.name = name;
return this;
}
/**
* Type builder for use in Deserialization. For convenience, the type is also
* checked to determine whether it is a primitive type, setting the value of
* primitive appropriately.
* @param type
* @return builder
*/
@JsonAlias({"fieldType"})
public FieldBuilder jsonType(String type) {
this.type = type;
if (stringRepresentsPrimitive(type)) {
this.primitive = true;
}
return this;
}
/**
* Default value builder for use in Deserialization.
*
* Values are passed through as is, similar to the behavior of the Raw
* defaultValue builder.
* @param defaultValue
* @return builder
*/
public FieldBuilder jsonDefaultValue(String defaultValue) {
this.defaultValue = defaultValue;
return this;
}
/**
* Nullable builder for use in Deserialization.
* @param nullable
* @return builder
*/
public FieldBuilder jsonNullable(boolean nullable) {
this.nullable = nullable;
return this;
}
/**
* Primitive builder for use in Deserialization.
* @param primitive
* @return builder
*/
public FieldBuilder jsonPrimitive(boolean primitive) {
this.primitive = primitive;
return this;
}
/**
* Match builder for use in Deserialization.
*
* As a default was set for this property, setting it directly looks different
* than the others, requiring match$value to be set as well as match$set to be
* set to true.
* @param match
* @return
*/
public FieldBuilder jsonMatch(boolean match) {
this.match$value = match;
this.match$set = true;
return this;
}
}
/**
......@@ -129,9 +214,27 @@ public class Field {
/**
* Convenience method for creating Raw value.
* @param value
* @return
* @return new Raw object
*/
public static Raw raw(String value) {
return new Raw(value);
}
/**
* Determine whether the passed string represents a primitive data type, i.e.
* byte, short, int, long, float, double, boolean, or char.
*
* @param type
* @return primitive data type or not
*/
private static boolean stringRepresentsPrimitive(String type) {
return type.equals("byte") ||
type.equals("short") ||
type.equals("int") ||
type.equals("long") ||
type.equals("float") ||
type.equals("double") ||
type.equals("boolean") ||
type.equals("char");
}
}
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment